65.9K
CodeProject 正在变化。 阅读更多。
Home

使用任务工厂进行多线程处理,C#,基本示例

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.65/5 (30投票s)

2014年11月24日

CPOL

7分钟阅读

viewsIcon

110488

downloadIcon

2733

这是一个小型基本示例,展示了如何使用新的 C# 任务工厂快速设置多线程环境。

引言

在我之前的文章(使用 Background Worker 进行多线程处理,C#)中,我讨论了 Background Worker,它是创建多线程桌面应用程序最简单有效的方法。然而,随着新的任务工厂的出现,这项技术正在过时。关于这个主题有许多很好的文章,详细且组织良好,例如:任务并行库和 async-await 功能 - 简单示例中的使用模式,以及 6 部分教程:任务并行库:1/n。然而,我希望这篇文章是我之前文章的续集,并向您展示如何使用任务工厂的基本功能。我认为从 Background Worker 迁移过来的用户会发现这很容易理解,因为我写得尽可能接近我们在 Background Worker 中使用的技术,我打算不使用 Lambda 表达式,尽管我鼓励您使用它们。

背景

编写过桌面应用程序的开发人员都知道执行繁重操作或从远程位置请求数据时的感受。用户界面会冻结直到操作完成。这是因为应用程序在单个线程上运行,当该线程忙于处理其他事情时,应用程序会冻结直到该线程再次准备好将用户界面渲染给用户。在这里,多线程就派上用场了:您将繁重的工作委托给另一个线程,因此您的应用程序在操作期间将保持响应。您还可以利用用户机器的多处理能力将任务分配给不同的线程/任务。对于我们的示例,我们将使用 任务工厂 类。

实现示例

我将使用 Visual Studio 2012 来完成这个项目。不幸的是,这里使用的一些功能在 Visual Studio 2010 或更早版本中不可用(例如 async 关键字),或者在最好的情况下,很难添加并且最终会导致隐藏的错误。

首先创建一个新的 Windows 窗体应用程序,并按照下面的方式设置您的设计。我个人喜欢使用 TableLayoutPanel 来设计我的窗体,您可以查看我写的一个小技巧,它能让您更好地了解这个控件:使用 TableLayoutPanel 设计 Windows 窗体的布局,带有自动展开面板

基本上,我们将有一个文本框(设置为多行模式)来显示来自工作线程的结果,一个数字框允许我们选择一个数字,一个开始按钮和一个取消按钮。我们还将有一个带有状态标签的状态栏,以显示任务的进度。

从工具箱菜单的“菜单和工具栏”部分,添加一个“状态栏”。

 

在状态栏内,单击左上角的小箭头并添加一个“状态标签”。将标签重命名为 lblStatus,并将其 Text 属性设置为空 string

在我们开始之前,请记住只有主线程才能访问用户控件,因此我们必须从主线程捕获用户输入,并以某种方式将其传递给后台线程。

右键单击您的表单并选择“查看代码”,然后输入以下方法

 private async void RunTask()
        {
            int numericValue = (int)numericUpDown1.Value;//Capture the user input
            object[] arrObjects = new object[] { numericValue };//Declare the array of objects

            using (Task<string> task = new Task<string>(new Func<object, 
            string>(PerfromTaskAction), arrObjects, cancellationToken))//Declare and 
                                                                       //initialize the task
            {
                lblStatus.Text = "Started Calculation...";//Set the status label to signal 
                                                          //starting the operation
                btnStart.Enabled = false; //Disable the Start button
                task.Start();//Start the execution of the task
                await task;// wait for the task to finish, without blocking the main thread

                if (!task.IsFaulted)
                {
                    textBox1.Text = task.Result.ToString();//at this point, 
                    //the task has finished its background work, and we can take the result
                    lblStatus.Text = "Completed.";//Signal the completion of the task
                }
                btnStart.Enabled = true; //Re-enable the Start button
            }
        }

在这里,我们首先从数字框获取用户输入,创建了一个对象数组,然后将数字框中的值添加到该数组中。我们将把这个对象数组传递给后台线程,因为只有主线程才能访问用户控件。之后,我们将初始化一个 Task 对象,其中 表示我们的 Task 将返回一个 String 对象。

之后,我们将状态标签设置为“开始计算...”,以向用户发出信号,表明我们的后台操作已开始。然后我们将使用 task.Start(); 启动我们的任务。然后我们将使用 await 命令等待任务。这与 wait 命令不同,因为它不会阻塞主线程,执行将异步完成,因此在方法声明中使用 async 关键字。

现在,在编写将在后台线程中执行的代码之前,让我们编写一个简单的方法来模拟一个繁重操作(调用远程服务器、从数据库请求数据、复杂操作...),我们只需在返回结果之前调用 Thread.Sleep 100 毫秒

private int PerformHeavyOperation(int i)
        {
            System.Threading.Thread.Sleep(100);
            return i * 1000;
        }

现在,我们创建将在后台线程中由任务执行的方法,类似于后台工作器中的 DoWork 事件

private string PerfromTaskAction(object state)
        {
            object[] arrObjects = (object[])state;//Get the array of objects from the main thread
            int maxValue = (int)arrObjects[0];//Get the maxValue integer from the array of objects

            StringBuilder sb = new StringBuilder();//Declare a new string builder to build the result

            for (int i = 0; i < maxValue; i++)
            {
                sb.Append(string.Format("Counting Number: {0}{1}", 
                PerformHeavyOperation(i), 
                Environment.NewLine));//Append the result to the string builder
            }

            return sb.ToString();//return the result
        }

最后,双击开始按钮,并在该按钮的点击事件处理程序中输入以下内容。这将启动我们的任务。

  private void btnStart_Click(object sender, EventArgs e)
        {
            RunTask();
        }

运行表单,然后单击开始,您会注意到表单将开始计算,在计算期间保持响应,最后将显示您想要的结果。

从任务中报告进度

如果我们可以向用户显示操作的进度,例如状态消息或加载进度条,那将是很好的。正如我们之前提到的,我们无法直接从后台线程访问用户界面,因此我们必须找到一种方法从后台线程向主线程报告进度。为此,我们将使用 Progress 对象。在我的示例中,我将以 string 形式报告进度,因此在代码顶部声明一个类型为 Progress 的对象,如下所示

Progress<string> progressManager = 
new Progress<string>();//Declare the object that will manage progress, and 
		//will be used to get the progress form our background thread

在表单构造函数中,添加以下代码行,这将设置进度更改事件。

progressManager.ProgressChanged += progressManager_ProgressChanged;//Set the Progress changed event

表单构造函数现在将如下所示

 public Form1()
        {
            InitializeComponent();
            progressManager.ProgressChanged += progressManager_ProgressChanged;//Set the 
                                                                               //Progress changed event
        }

实现 ProgressChanged 事件,我们只是将从后台线程接收到的文本设置到我们的状态标签中。这个事件是在主线程中触发的,这就是我们能够访问状态标签的原因。

 void progressManager_ProgressChanged(object sender, string e)
        {
            lblStatus.Text = e;
        }

将您的 perform Task Action 方法更改为以下内容,注意我们如何使用进度管理器从后台线程报告进度

     private string PerfromTaskAction(object state)
        {
            object[] arrObjects = (object[])state;//Get the array of objects from the main thread
            int maxValue = (int)arrObjects[0];//Get the maxValue integer from the array of objects

            StringBuilder sb = new StringBuilder();//Declare a new string builder to build the result

            for (int i = 0; i < maxValue; i++)
            {
                sb.Append(string.Format("Counting Number: {0}{1}", 
                PerformHeavyOperation(i), Environment.NewLine));//Append the result 
                                                                //to the string builder
                ((IProgress<string>)progressManager).Report(string.Format
                ("Now Counting number: {0}...", i));//Report our progress to the main thread
            }

            return sb.ToString();//return the result
        }

现在运行您的表单,您会注意到标签将显示进度更新

取消正在运行的任务

允许用户取消耗时过长或用户不再对结果感兴趣的任务,这总是很受欢迎的。

要取消正在运行的任务,我们需要一个 CancellationToken,而要获取一个取消令牌,我们需要一个取消令牌源。幸运的是,有两个 Microsoft 类可以完全满足您的需求,即 CancellationTokenSourceCancellationToken。首先声明一个 CancellationTokenSource 对象,然后声明一个 CancellationToken 对象

 CancellationTokenSource cancellationTokenSource; //Declare a cancellation token source
 CancellationToken cancellationToken; //Declare a cancellation token object, 
 	//we will populate this token from the token source, and pass it to the Task constructor.

双击您的“取消”按钮并在事件处理程序中添加以下代码行,这将向取消令牌发出取消请求

cancellationTokenSource.Cancel();

在您的 RunTask 方法中,将取消令牌添加到 Task 的构造函数中,同时初始化 CancellationTokenSource 对象,并为 CancellationToken 赋予一个新的令牌。我们必须在每次任务启动之前执行此操作,因为取消令牌在取消后无法重复使用,如果我们尝试运行一个其取消令牌处于已取消状态的任务,您将收到运行时错误。

 private async void RunTask()
        {
            int numericValue = (int)numericUpDown1.Value;//Capture the user input
            object[] arrObjects = new object[] { numericValue };//Declare the array of objects

            //Because Cancellation tokens cannot be reused after they have been canceled, 
            //we need to create a new cancellation token before each start
            cancellationTokenSource = new CancellationTokenSource();
            cancellationToken = cancellationTokenSource.Token;

            using (Task<string> task = new Task<string>(new Func<object, 
            string>(PerfromTaskAction), arrObjects, cancellationToken))//Declare and initialize the task
            {
                lblStatus.Text = "Started Calculation...";//Set the status label to signal 
                                                          //starting the operation
                btnStart.Enabled = false; //Disable the Start button
                task.Start();//Start the execution of the task
                await task;// wait for the task to finish, without blocking the main thread

                if (!task.IsFaulted)
                {
                    textBox1.Text = task.Result.ToString();//at this point, 
                    	//the task has finished its background work, and we can take the result
                    lblStatus.Text = "Completed.";//Signal the completion of the task
                }

                btnStart.Enabled = true; //Re-enable the Start button
            }
        }

更改您的 PerformTaskAction 方法,以在循环的每次迭代中检查取消请求,如果您发现用户发出了取消请求,则中断循环,从而结束后台线程的执行。您可以通过检查 CancellationTokenIsCancellationRequested 属性来检查是否有取消请求待处理。另一种方法是使用 CancellationToken.ThrowIfCancellationRequested() 方法抛出一个 AggregateException,该异常将停止您的后台线程,您可以从主线程捕获此异常以知道任务已被取消。

private string PerfromTaskAction(object state)
        {
            object[] arrObjects = (object[])state;//Get the array of objects from the main thread
            int maxValue = (int)arrObjects[0];    //Get the maxValue integer from the array of objects

            StringBuilder sb = new StringBuilder();//Declare a new string builder to build the result

            for (int i = 0; i <= maxValue; i++)
            {
                if (cancellationToken.IsCancellationRequested)//Check if a cancellation request 
                                                              //is pending
                {
                    break;
                }
                else
                {
                    sb.Append(string.Format("Counting Number: {0}{1}", 
                    PerformHeavyOperation(i), Environment.NewLine));//Append the result 
                                                                    //to the string builder
                    ((IProgress<string>)progressManager).Report(string.Format
                    ("Now Counting number: {0}...", i));//Report our progress to the main thread
                }
            }

            return sb.ToString();//return the result
        }

现在试一试,运行表单并在任务运行时尝试取消它,并检查您的代码是否有效。您可以从链接顶部下载完整的示例源代码,感谢您的关注:)

© . All rights reserved.