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

使用后台工作线程进行多线程处理,C#

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.65/5 (48投票s)

2014 年 11 月 12 日

CPOL

7分钟阅读

viewsIcon

209550

downloadIcon

7632

本文将介绍如何利用后台工作线程最重要的功能,将代码的负载分配到不同的工作线程中。它还将演示如何从后台工作线程接收进度事件以及如何取消当前正在运行的线程。

引言

在开发 Windows Forms 应用程序时,您会注意到当应用程序执行耗时操作(例如处理大型文件或从远程服务器请求数据)时,用户界面会冻结。这是因为您的应用程序运行在单个线程上。该线程负责向用户呈现用户界面,并处理应用程序的所有事件和方法。因此,耗时的操作会阻塞您的用户界面,直到完成为止。今天,我们将把这些耗时的操作移到另一个线程上,从而在用户界面保持流畅的同时,在后台进行操作。

背景

为此,我们将使用 Microsoft 的 BackgroundWorker 类,有关该类的更多信息可以在此处找到。

我们将创建一个简单的应用程序,该应用程序将执行一项耗时的操作,并将最终结果显示给用户。这项繁重的工作将在另一个线程上进行,并在其运行期间不断地将进度更新到用户界面。我们还将允许用户随时取消该操作。

请记住,只有主线程才能访问用户界面,换句话说,您无法从其他线程访问任何用户控件。我们稍后会对此进行更多介绍。

Using the Code

我将在进度中展示应用程序使用的代码,最后,我会附上最终的源代码。

创建解决方案

我们将从 Microsoft Visual Studio 创建一个简单的 Windows Forms 应用程序窗体开始,我将使用 Visual Studio 2010。创建一个新的 Windows Forms 应用程序,如图所示。我个人倾向于使用 C#,但您也可以使用 VB.NET。

按照下图设置您的窗体设计器。我个人喜欢使用表格布局面板来组织我的控件。这样,如果窗体被扩展或调整大小,我也可以更容易地保持控件的顺序。我们需要添加一个文本框(设置为多行模式),用于显示来自工作线程的结果;一个数字框,用于选择一个数字;一个开始按钮和一个取消按钮。

从工具箱菜单的“菜单和工具栏”部分,添加一个“状态栏”。这将允许我们添加一个状态标签,用于向最终用户显示进度。

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

在代码类中,声明一个 BackgroundWorker 类型的对象。

private BackgroundWorker myWorker = new BackgroundWorker();

在窗体构造函数中,初始化我们刚创建的工作线程的以下属性。

  • DoWork 事件处理程序,当指示后台工作线程开始异步工作时将被调用。在此事件中,我们执行耗时的操作,例如调用远程服务器、查询数据库、处理文件……此事件将在新线程上调用,这意味着我们无法从该方法内部访问用户控件。
  • RunWorkerCompleted 事件处理程序,当后台工作线程完成执行、被取消或引发异常时发生。此事件将在主线程上调用,这意味着我们可以从该方法内部访问用户控件。
  • ProgressChanged 事件处理程序,当调用后台工作线程的 ReportProgress 方法时发生。我们使用此方法将进度写入用户界面。此事件将在主线程上调用,这意味着我们可以从该方法内部访问用户控件。
  • WorkerReportsProgress 属性,需要此属性来指示工作线程可以向主线程报告进度。
  • WorkerSupportsCancellation 属性,需要此属性来指示工作线程可以根据用户请求被取消。

以下是声明后的构造函数的完整代码。

public Form1()
{
    InitializeComponent();

    myWorker.DoWork+=new DoWorkEventHandler(myWorker_DoWork);
    myWorker.RunWorkerCompleted+=new RunWorkerCompletedEventHandler(myWorker_RunWorkerCompleted);
    myWorker.ProgressChanged+=new ProgressChangedEventHandler(myWorker_ProgressChanged);
    myWorker.WorkerReportsProgress = true;
    myWorker.WorkerSupportsCancellation = true;
}

现在让我们声明我们工作线程的事件处理程序。

  • DoWork 事件处理程序将接受两个参数:一个 sender 对象和一个 DoWorkEventArgs 参数。
    protected void myWorker_DoWork(object sender, DoWorkEventArgs e)
    {
    
    }
  • RunWorkerCompleted 事件处理程序将接受两个参数:一个 sender 对象和一个 RunWorkerCompletedEventArgs 参数:?
    protected void myWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
    
    }
  • ProgressChanged 事件处理程序将接受两个参数:一个 sender 对象和一个 ProgressChangedEventArgs 参数:?
    protected void myWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
    {
    
    }

现在我们将创建一个辅助方法,该方法只接受一个整数,将该整数乘以 1000,暂停 250 毫秒,然后返回结果。这只是对您的应用程序可能执行的繁重操作的一种模拟,您可以将睡眠时间更改为任何值。请注意,由于此方法将在 DoWork 事件内部调用,因此是 Background 线程将休眠给定的时间,而不是主线程。该函数如下所示。

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

切换到设计器模式,双击“开始”按钮开始处理其单击事件。我们将捕获数字上/下控件中的数值,将该值传递给异步线程,并指示后台工作线程开始工作。我们需要在此时捕获数值,因为一旦进入新线程,我们将无法捕获用户控件的任何输入。要启动后台线程的执行,我们将调用 RunWorkerAsync 方法。此方法可以接受一个对象参数,该对象将被传递到后台线程。在此对象中,我们可以放入从用户控件捕获的任何值。要传递多个值,可以使用对象数组。以下是 btnStart_Click 事件处理程序的完整代码。请注意,正在进行的任务不能再次调用,如果尝试这样做,将导致运行时错误。

private void btnStart_Click(object sender, EventArgs e)
{
    int numericValue = (int)numericUpDownMax.Value;//Capture the user input
    object[] arrObjects = new object[] { numericValue };//Declare the array of objects
    if (!myWorker.IsBusy)//Check if the worker is already in progress
    {
        btnStart.Enabled = false;//Disable the Start button
        myWorker.RunWorkerAsync(arrObjects);//Call the background worker
    }
}

现在在 DoWork 事件处理程序中,我们将完成所有繁重的工作。首先,我们将捕获从主线程接收的对象,然后对其进行处理,最后将结果传递回主线程,以便能够将其显示给用户。请记住,只有主线程可以访问用户控件。另外,在处理值的过程中,我们将持续执行两项操作:

  • 使用 ReportProgress 方法向主线程报告进度,以向用户显示我们当前的位置。
  • 检查后台工作线程的 CancellationPending 属性,以检查用户是否已发出取消命令。

最后,我们将把结果放在 DoWorkEventArgs 参数的 Result 属性中,以便在 RunWorkerCompleted 事件中被主线程捕获。以下是我们 DoWork 处理程序的完整代码。

protected void myWorker_DoWork(object sender, DoWorkEventArgs e)
{
    BackgroundWorker sendingWorker = 
    (BackgroundWorker)sender;//Capture the BackgroundWorker that fired the event
    object[] arrObjects = 
    (object[])e.Argument;//Collect the array of objects the we received from the main thread

    int maxValue = (int)arrObjects[0];//Get the numeric value 
            //from inside the objects array, don't forget to cast
    StringBuilder sb = new StringBuilder();//Declare a new string builder to store the result.

    for (int i = 1; i <= maxValue; i++)//Start a for loop
    {
        if (!sendingWorker.CancellationPending)//At each iteration of the loop, 
                    //check if there is a cancellation request pending 
        {
            sb.Append(string.Format("Counting number: {0}{1}", 
            PerformHeavyOperation(i), Environment.NewLine));//Append the result to the string builder
            sendingWorker.ReportProgress(i);//Report our progress to the main thread
        }
        else
        {
            e.Cancel = true;//If a cancellation request is pending, assign this flag a value of true
            break;// If a cancellation request is pending, break to exit the loop
        }
    }

    e.Result = sb.ToString();// Send our result to the main thread!
}

现在我们将处理 ProgressChanged 事件。在这里,我们只捕获在调用 ReportProgress 方法时从后台线程发送的整数值。请注意,您可以使用 ProgressChangedEventArgs 参数的 UserState 来传递任何 datatype 的对象。除了显示状态消息外,您还可以使用进度条来显示当前进度。

protected void myWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
    //Show the progress to the user based on the input we got from the background worker
    lblStatus.Text = string.Format("Counting number: {0}...", e.ProgressPercentage);
}

现在是 RunWorkerCompleted 事件。在这里,我们首先需要检查工作线程是否已被取消,或者是否发生了错误。之后,我们将收集后台工作线程为我们计算的结果,并将其显示给用户。

protected void myWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    if (!e.Cancelled && 
    e.Error == null)//Check if the worker has been canceled or if an error occurred
    {
        string result = (string)e.Result;//Get the result from the background thread
        txtResult.Text = result;//Display the result to the user
        lblStatus.Text = "Done";
    }
    else if (e.Cancelled)
    {
        lblStatus.Text = "User Canceled";
    }
    else
    {
        lblStatus.Text = "An error has occurred";
    }
    btnStart.Enabled = true;//Re enable the start button
}

最后还有一件事,我们需要实现取消按钮。双击“取消”按钮,并在代码类中调用后台工作线程的 CancelAsync 方法。这将把 CancellationPending 标志设置为 true。我们在 DoWork 事件处理程序的每个循环迭代中都检查过这个标志。由此,我们可以得出结论,终止一个正在进行的 backgroundworker 不会立即发生;如果后台工作线程正在执行某项操作,我们必须等待该工作线程完成它,然后才能取消操作。以下是 btnCancel_Click 的代码。

private void btnCancel_Click(object sender, EventArgs e)
{
    myWorker.CancelAsync();//Issue a cancellation request to stop the background worker
}

 

最后,这是应用程序运行过程中的一个快照。

这是操作完成时的快照。

您可以从本文顶部的链接下载源代码,感谢您的关注。 :)

下一篇:查看我关于新的 C# 任务工厂的更新的基本示例文章:

 

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

© . All rights reserved.