-
异步杂项:

使用BackgroundWorker进行多线程处理

默认情况下,当你的程序执行一段代码时,这段代码会被运行在和程序自身相同的线程上。这意味着当这代码段被执行时,程序内部不会再执行其他任何工作,包括更新UI。

对于刚接触Windows编程的人来说,当他们第一次执行需要超过一秒钟的事情并且意识到他们的应用程序在执行此操作时实际挂起时,这是一个令人惊讶的事情。 结果是来自那些试图在更新进度条的同时运行冗长进程的人们发布了许多令人沮丧的论坛帖子,只是意识到在进程运行完成之前进度条没有更新。

所有这一切的解决方案是使用多个线程,C#很容易做到,但多线程带来了很多陷阱,对于很多人来说,它们并不是那么容易理解。 这就是BackgroundWorker发挥作用的地方 - 它使您在应用程序中使用额外的线程变得简单,容易和快速。

BackgroundWorker的工作原理

关于Windows应用程序中多线程的最困难的概念是,不允许您从另一个线程更改UI - 如果这样做,应用程序将立即崩溃。 相反您必须在UI(主)线程上调用一个方法,然后进行所需的更改。 这有点麻烦,但在使用BackgroundWorker时则不会。

在不同的线程上执行任务时,通常需要在两种情况下与应用程序的其余部分进行通信:当您想要更新它以显示您在此过程中的进度时,当然还有任务完成时你想要显示结果。 BackgroundWorker是围绕这个想法构建的,因此附带了两个事件ProgressChangedRunWorkerCompleted

第三个事件是DoWork,一般规则是您不能在此事件涉及UI中的任何内容。 您应该调用ReportProgress()方法,该方法会引发ProgressChanged事件,您可以从中更新UI。 完成后,将结果分配给worker,然后引发RunWorkerCompleted事件。

总而言之,DoWork事件负责所有冗长的工作。 其中的所有代码都在另一个线程上执行,因此不允许您从中涉及UI。 您通过使用RunWorkerAsync()方法上的参数将数据(从UI或其他地方)引入事件,并通过将数据分配给e.Result属性来生成数据。

另一方面ProgressChangedRunWorkerCompleted事件在创建BackgroundWorker的同一线程上执行,该线程通常是主/ UI线程,因此您可以从它们更新UI。 可以在运行的后台任务和UI之间执行的唯一通信是通过ReportProgress()方法。

这是很多理论,但即使BackgroundWorker易于使用,理解其工作方式和内容也很重要,因此您不会意外地做错事 - 如前所述,多线程中的错误可能会导致一些令人讨厌的问题。

BackgroundWorker示例

已经讲了很多理论 - 让我们看看它是什么。 在第一个例子中,我们想要一个非常简单但耗时的工作。 对0到10,000之间的每个数字都进行测试,看它是否可以用数字7整除。这对于今天的快速计算机来说实际上是小菜一碟,所以为了让它更耗时,从而更容易证明我们的观点,我在每次迭代中都添加了一毫秒的延迟。

我们的示例应用程序有两个按钮:一个将同步执行任务(在同一个线程上),另一个将使用BackgroundWorker执行任务,从而在不同的线程上执行任务。 很容易看到在执行耗时的任务时需要额外的线程。 代码如下所示:

<Window x:Class="WpfTutorialSamples.Misc.BackgroundWorkerSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="BackgroundWorkerSample" Height="300" Width="375">
    <DockPanel Margin="10">
        <DockPanel DockPanel.Dock="Top">
            <Button Name="btnDoSynchronousCalculation" Click="btnDoSynchronousCalculation_Click" DockPanel.Dock="Left" HorizontalAlignment="Left">Synchronous (same thread)</Button>
            <Button Name="btnDoAsynchronousCalculation" Click="btnDoAsynchronousCalculation_Click" DockPanel.Dock="Right" HorizontalAlignment="Right">Asynchronous (worker thread)</Button>
        </DockPanel>
        <ProgressBar DockPanel.Dock="Bottom" Height="18" Name="pbCalculationProgress" />

        <ListBox Name="lbResults" Margin="0,10" />

    </DockPanel>
</Window>
using System;
using System.ComponentModel;
using System.Windows;

namespace WpfTutorialSamples.Misc
{
	public partial class BackgroundWorkerSample : Window
	{
		public BackgroundWorkerSample()
		{
			InitializeComponent();
		}

		private void btnDoSynchronousCalculation_Click(object sender, RoutedEventArgs e)
		{
			int max = 10000;
			pbCalculationProgress.Value = 0;
			lbResults.Items.Clear();

			int result = 0;
			for(int i = 0; i < max; i++)
			{
				if(i % 42 == 0)
				{
					lbResults.Items.Add(i);
					result++;
				}
				System.Threading.Thread.Sleep(1);
				pbCalculationProgress.Value = Convert.ToInt32(((double)i / max) * 100);
			}
			MessageBox.Show("Numbers between 0 and 10000 divisible by 7: " + result);
		}

		private void btnDoAsynchronousCalculation_Click(object sender, RoutedEventArgs e)
		{
			pbCalculationProgress.Value = 0;
			lbResults.Items.Clear();

			BackgroundWorker worker = new BackgroundWorker();
			worker.WorkerReportsProgress = true;
			worker.DoWork += worker_DoWork;
			worker.ProgressChanged += worker_ProgressChanged;
			worker.RunWorkerCompleted += worker_RunWorkerCompleted;
			worker.RunWorkerAsync(10000);
		}

		void worker_DoWork(object sender, DoWorkEventArgs e)
		{
			int max = (int)e.Argument;
			int result = 0;
			for(int i = 0; i < max; i++)
			{
				int progressPercentage = Convert.ToInt32(((double)i / max) * 100);
				if(i % 42 == 0)
				{
					result++;
					(sender as BackgroundWorker).ReportProgress(progressPercentage, i);
				}
				else
					(sender as BackgroundWorker).ReportProgress(progressPercentage);
				System.Threading.Thread.Sleep(1);

			}
			e.Result = result;
		}

		void worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
		{
			pbCalculationProgress.Value = e.ProgressPercentage;
			if(e.UserState != null)
				lbResults.Items.Add(e.UserState);
		}

		void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
		{
			MessageBox.Show("Numbers between 0 and 10000 divisible by 7: " + e.Result);
		}

	}
}

XAML部分由几个按钮组成,一个用于同步运行进程(在UI线程上),一个用于异步运行(在后台线程上),一个ListBox控件用于显示所有计算出的数字,然后在窗口底部显示一个ProgressBar控件,以显示......好吧,进度!

在后台代码,我们从同步事件处理程序开始。 如上所述,它在0到10.000之间循环,每次迭代都有一个小延迟,如果数字可以用数字7整除,那么我们将它添加到列表中。 在每次迭代中,我们还会更新ProgressBar,一旦完成,我们会向用户显示有关已找到的数量的消息。

如果您运行应用程序并按下第一个按钮,无论您在此过程中的进度如何,它都将如下所示:

列表中没有项,并且ProgressBar没有进度,按钮甚至没有弹起,这证明自从鼠标按下按钮以来没有对UI进行过哪怕一次更新。

按第二个按钮将使用BackgroundWorker处理。 从代码几乎一样,但方式略有不同。 现在所有的冗长工作都放在DoWork事件中,运行RunWorkerAsync()方法后worker被调用。 这个方法从您的应用程序中获取可供worker使用的输入,我们将在稍后讨论。

如前所述,不允许从DoWork事件更新UI。 相反,我们在worker上调用ReportProgress方法。 如果当前数字可以用7整除,我们将其添加到列表中 - 否则我们只报告当前进度百分比,以便可以更新ProgressBar。

测试完所有数字后,我们将结果分配给e.Result属性。 然后将其传送到RunWorkerCompleted事件,我们将向用户显示该事件。 这可能看起来有点麻烦,而不是仅在工作完成后立即向用户显示,再次确保我们不会从DoWork事件中与UI进行通信,这是不允许的。

结果正如您所看到的,界面更加友好:

窗口不再挂起,单击按钮未被抑制,可能的数字列表会立即更新,ProgressBar会稳步上升 - 界面响应速度更快。

输入和输出

请注意,传递给RunWorkerAsync()方法的参数形式的输入以及分配给DoWork事件的e.Result属性的值形式的输出都是对象类型。 这意味着您可以为它们分配任何类型的值。 我们的示例是基础的,输入和输出都可以包含在一个整数值中,但是输入和输出可以是更复杂的类型。

这是通过使用更复杂的类型来完成的,在许多情况下是结构甚至是您自己创建并传递的类。 通过这样做,可能性几乎是无穷无尽的,您可以在BackgroundWorker和应用程序/ UI之间传输尽可能多的复杂数据。

ReportProgress方法实际上也是如此。 它的辅助参数称为userState,是一种对象类型,这意味着您可以将任何想要的内容传递给ProgressChanged方法。

小结

当您在应用程序中需要多线程时,BackgroundWorker是一个出色的工具,主要是因为它易于使用。 在本章中,我们看到了BackgroundWorker非常容易实现的一个事情就是报告进度,对取消正在运行的任务的支持也非常方便。 我们将在下一章研究。