概述
在应用程序中,可能会遇到一些执行耗时的功能操作,比如数据下载、复杂计算及数据库事务等,一般这样的功能会在单独的线程上实现,执行结束后结果显示到用户界面上,这样可避免造成用户界面长时间无响应情况。在.NET 2.0及以后的版本中,FCL提供了BackgroundWorker组件来方便的实现这些功能要求。
组件介绍
BackgroundWorker类位于System.ComponentModel 命名空间中,通过该类在单独的线程上执行操作实现基于事件的异步模式。下面对BackgroundWorker类的主要成员进行介绍。
BackgroundWorker类的第1个主要方法是RunWorkerAsync,该方法提交一个以异步方式启动运行操作的请求,发出请求后,将引发 DoWork 事件,在事件处理程序中开始执行异步操作代码。RunWorkerAsync 方法签名如下:
1 | public void RunWorkerAsync(); |
2 | public void RunWorkerAsync(Object argument); |
果异步操作需要操作参数,可以将其作为argument参数提供,由于参数类型为Object,因此访问时可能需要进行类型转换。
CancelAsync 方法提交终止异步操作的请求,并将 CancellationPending 属性设置为 true。需要注意的是,CancelAsync 方法是否调用成功,同WorkerSupportsCancellation 属性相关,如果允许取消执行的异步操作,需将WorkerSupportsCancellation 属性设置为true,否则调用该方法将抛出异常。CancelAsync方法不含参数,方法签名如下:
1 | public void CancelAsync(); |
调用 CancelAsync 方法时,BackgroundWorker的 CancellationPending 属性值将被设置为true,因此在编写单独线程中执行的辅助方法时,代码中应定期检查 CancellationPending 属性,查看是否已将该属性设置为 true,如果为true,应该结束辅助方法的执行。有一点需要注意的是,DoWork 事件处理程序中的代码有可能在发出取消请求时已经完成处理工作,因此,DoWork事件处理程序或辅助方法可能会错过设置CancellationPending属性为true的时机。在这种情况下,即使调用 CancelAsync方法发出了取消异步操作请求,RunWorkerCompleted 事件处理程序中RunWorkerCompletedEventArgs 参数的 Cancelled 标志也不会被设置为 true,这是在多线程编程中经常会出现的竞争条件问题,因此编写代码的时候需要考虑。
在执行异步操作时,如果需要跟踪异步操作执行进度,BackgroundWorker类提供了 ReportProgress 方法,调用该方法将引发 ProgressChanged 事件,通过注册该事件在事件处理程序中获取异步执行进度信息。方法签名如下:
1 | public void ReportProgress(int percentProgress); |
2 | public void ReportProgress(int percentProgress,Object userState); |
该方法包含两个版本,percentProgress表示进度百分比,取值为0-100,userState为可选参数表示自定义用户状态。
同CancelAsync 方法一样,BackgroundWorker的WorkerReportsProgress 属性设置为 true时,ReportProgress 方法才会调用成功,否则将引发InvalidOperationException异常。
上面已经提到了BackgroundWorker的3个属性,CancellationPending用来提示操作是否已经取消,WorkerReportsProgress和WorkerSupportsCancellation分别用来设置是否允许进度汇报和进行取消操作。
1 | public bool CancellationPending { get; } |
2 | public bool WorkerReportsProgress { get; set; } |
3 | public bool WorkerSupportsCancellation { get; set; } |
另外一个会用到的属性是IsBusy:
1 | public bool IsBusy { get; } |
通过该属性查询BackgroundWorker实例是否正在运行异步操作,如果 BackgroundWorker 正在运行异步操作,则为true,否则为false。
BackgroundWorker类包含3个事件,在事件处理程序中可进行异步操作辅助代码编写和同用户界面信息交互。
1 | public event DoWorkEventHandler DoWork; |
2 | public event ProgressChangedEventHandler ProgressChanged; |
3 | public event RunWorkerCompletedEventHandler RunWorkerCompleted; |
DoWork事件处理程序用来调用辅助方法进行实际处理操作,由于该事件处理程序在不同于UI的线程上执行,因此需要确保在 DoWork 事件处理程序中不操作任何用户界面对象。如果辅助方法需要参数支持,可以通过RunWorkerAsync方法传入,在 DoWork 事件处理程序中,通过 DoWorkEventArgs.Argument 属性提取该参数。在异步操作期间,可以通过 ProgressChanged事件处理程序获取异步操作进度信息,通过RunWorkerCompleted 事件处理程序获取异步操作结果信息,在ProgressChanged和RunWorkerCompleted的事件处理程序中可以安全的同用户界面进行通信。
应用示例
下面通过一个简单的示例来演示BackgroundWorker组件的典型应用。在本示例中,实现一个数值的求和操作,该操作本身运行很快,为模拟处理过程有一个可感知的时间段,在辅助方法中调用了Thread.Sleep方法。
示例程序通过Windows Forms展示,显示了对1-100的数值进行求和操作,界面如下:
图1:应用程序界面
下面对主要实现代码进行说明,先看一下BackgroundWorker类的初始化,在初始化过程中注册了3个事件,允许异步辅助方法调用,以及异步操作进度通知和操作取消。
1 | private System.ComponentModel.BackgroundWorker backgroundWorker1; |
2 | private void InitializeBackgoundWorker() |
3 | { |
4 | this.backgroundWorker1 = new System.ComponentModel.BackgroundWorker(); |
5 | this.backgroundWorker1.WorkerReportsProgress = true; |
6 | this.backgroundWorker1.WorkerSupportsCancellation = true; |
7 | this.backgroundWorker1.DoWork += new DoWorkEventHandler(backgroundWorker1_DoWork); |
8 | this.backgroundWorker1.ProgressChanged += new ProgressChangedEventHandler(backgroundWorker1_ProgressChanged); |
9 | this.backgroundWorker1.RunWorkerCompleted += new RunWorkerCompletedEventHandler(backgroundWorker1_RunWorkerCompleted); |
10 | } |
通过StartAsync按钮事件处理程序开始异步处理操作请求,事件处理程序如下:
1 | private void startAsyncButton_Click(object sender, EventArgs e) |
2 | { |
3 | resultLabel.Text = String.Empty; |
4 | this.numericUpDown1.Enabled = false; |
5 | this.startAsyncButton.Enabled = false; |
6 | this.cancelAsyncButton.Enabled = true; |
7 | //获取计算数值. |
8 | int numberToCompute = (int)numericUpDown1.Value; |
9 | //启动异步操作. |
10 | backgroundWorker1.RunWorkerAsync(numberToCompute); |
11 | } |
startAsyncButton_Click处理程序首先对一些界面控件进行状态设置,然后调用BackgroundWorker实例的RunWorkerAsync方法开始执行异步操作,而此时就会触发DoWork事件。
1 | void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e) |
2 | { |
3 | BackgroundWorker worker = sender as BackgroundWorker; |
4 | e.Result = ComputeAdd((int)e.Argument, worker, e); |
5 | } |
在DoWork事件处理程序中,通过DoWorkEventArgs.Argument属性获取传入的参数传递给ComputeAdd辅助方法,并把处理结果保存到DoWorkEventArgs.Result属性中,最后在RunWorkerCompleted 事件处理程序的RunWorkerCompletedEventArgs.Result 属性中获取处理结果。如果在DoWork事件处理程序中出现异常,则 BackgroundWorker 将捕获该异常并将其传递到 RunWorkerCompleted 事件处理程序,在该事件处理程序中,异常信息作为 RunWorkerCompletedEventArgs 的 Error 属性公开。
1 | private long ComputeAdd(int n, BackgroundWorker worker, DoWorkEventArgs e) |
2 | { |
3 | long result = 0; |
4 | for (int i = 1; i <= n; i++) |
5 | { |
6 | if (worker.CancellationPending) |
7 | { |
8 | e.Cancel = true; |
9 | break; |
10 | } |
11 | else |
12 | { |
13 | result += i; |
14 | Thread.Sleep(500); |
15 | int percentComplete = (int)((float)i / (float)n * 100); |
16 | worker.ReportProgress(percentComplete); |
17 | } |
18 | } |
19 | return result; |
20 | } |
在辅助方法中,代码定期访问BackgroundWorker实例的CancellationPending属性,如果调用了BackgroundWorker的CancelAsync 方法,那么CancellationPending属性值就会被设置为true,辅助方法就结束执行。另外,在辅助方法中实现了进度汇报功能,通过调用worker.ReportProgress方法触发ProgressChanged事件,接着通过ProgressChanged事件处理程序来更新进度显示。
1 | void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e) |
2 | { |
3 | this.progressBar1.Value = e.ProgressPercentage; |
4 | } |
最后,在RunWorkerCompleted事件处理程序中可以得到异步处理结果信息,分析异步操作是正常执行结束还是在处理中被取消或者是执行出现错误异常而终止。对于处理结果信息的访问有一个标准的顺序,先是判断异步处理是否异常结束,接着判断是否执行了取消操作,最后访问处理结果。
1 | void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) |
2 | { |
3 | if (e.Error != null) |
4 | { |
5 | MessageBox.Show(e.Error.Message); |
6 | } |
7 | else if (e.Cancelled) |
8 | { |
9 | resultLabel.Text = "Canceled"; |
10 | } |
11 | else |
12 | { |
13 | resultLabel.Text = e.Result.ToString(); |
14 | } |
15 | this.numericUpDown1.Enabled = true; |
16 | startAsyncButton.Enabled = true; |
17 | cancelAsyncButton.Enabled = false; |
18 | } |
上面的例子是在单个窗口中完成所有功能,可以对其进行简单的修改实现在独立对话框中显示进度并提供取消操作的功能。
图2:进度显示对话框
新建一个窗体命名为ProcessForm用来显示异步操作进度,对ProcessForm类的默认构造函数进行修改,传入BackgroundWorker实例的引用,注册ProgressChanged事件实现窗体进度条的更新,注册RunWorkerCompleted事件通知ProcessForm窗体关闭。
1 | public ProcessForm(BackgroundWorker backgroundWorker1) |
2 | { |
3 | InitializeComponent(); |
4 | this.backgroundWorker1 = backgroundWorker1; |
5 | this.backgroundWorker1.ProgressChanged += new ProgressChangedEventHandler(backgroundWorker1_ProgressChanged); |
6 | this.backgroundWorker1.RunWorkerCompleted += new RunWorkerCompletedEventHandler(backgroundWorker1_RunWorkerCompleted); |
7 | } |
8 | void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) |
9 | { |
10 | this.Close(); |
11 | } |
12 | |
13 | void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e) |
14 | { |
15 | this.progressBar1.Value = e.ProgressPercentage; |
16 | } |
17 | |
18 | private void cancelButton1_Click(object sender, EventArgs e) |
19 | { |
20 | this.backgroundWorker1.CancelAsync(); |
21 | this.cancelButton1.Enabled = false; |
22 | this.Close(); |
23 | } |
对于进度窗口的显示方式可以是模式窗口或非模式窗口,两者的实现代码并没有太大区别,改进后的StartAsync按钮事件处理程序如下。
1 | private void startAsyncButton_Click(object sender, EventArgs e) |
2 | { |
3 | // ... |
4 | backgroundWorker1.RunWorkerAsync(numberToCompute); |
5 | ProcessForm form = new ProcessForm(this.backgroundWorker1); |
6 | form.ShowDialog(this);//模式 |
7 | //form.Show(this);//非模式 |
8 | } |
实现原理
在分析BackgroundWorker实现原理之前,需要了解一下在.NET Framework 2.0版本中新增加的两个类。AsyncOperationManager 类和AsyncOperation 类都位于System.ComponentModel 命名空间中,AsyncOperation类提供了对异步操作的生存期进行跟踪的功能,包括操作进度通知和操作完成通知,并确保在正确的线程或上下文中调用客户端的事件处理程序。
1 | public void Post(SendOrPostCallback d,Object arg); |
2 | public void PostOperationCompleted(SendOrPostCallback d,Object arg); |
通过在异步辅助代码中调用Post方法把进度和中间结果报告给用户,如果是取消异步任务或提示异步任务已完成,则通过调用PostOperationCompleted方法结束异步操作的跟踪生命期。在PostOperationCompleted方法调用后,AsyncOperation对象变得不再可用,再次访问将引发异常。在两个方法中都包含SendOrPostCallback委托参数,签名如下:
1 | public delegate void SendOrPostCallback(Object state); |
SendOrPostCallback 委托用来表示在消息即将被调度到同步上下文时要执行的回调方法。
AsyncOperation类看上去很强大,不过有开发人员反映该类的.NET 2.0版本存在Bug,在3.0及后面的版本微软是否进行过更新还需进一步考证。笔者在控制台应用程序中进行测试,asyncOperation的Post方法递交的SendOrPostCallback委托不一定是在控制台主线程执行,通过访问System.Threading.Thread.CurrentThread.ManagedThreadId可以确认这一点,奇怪的是控制台程序未发现运行异常,这个可能是控制台程序执行方式不同于窗体程序的原因。
AsyncOperationManager 类为AsyncOperation对象的创建提供了便捷方式,通过CreateOperation方法可以创建多个AsyncOperation实例,实现对多个异步操作进行跟踪。
BackgroundWorker组件通过DoWork事件实现了在单独的线程上执行操作,其内部通过异步委托来完成,在BackgroundWorker类内部声明了WorkerThreadStartDelegate委托,并定义了threadStart成员变量,同时在构造函数中初始化threadStart。
1 | private delegate void WorkerThreadStartDelegate(object argument); |
2 | private readonly WorkerThreadStartDelegate threadStart; |
3 | public BackgroundWorker() |
4 | { |
5 | this.threadStart = new WorkerThreadStartDelegate(this.WorkerThreadStart); |
6 | //… |
7 | } |
BackgroundWorker通过调用RunWorkerAsync方法开始执行异步操作请求,并在方法体中调用threadStart.BeginInvoke方法实现异步调用。
1 | public void RunWorkerAsync(object argument) |
2 | { |
3 | if (this.isRunning) |
4 | { |
5 | throw new InvalidOperationException(SR.GetString("BackgroundWorker_WorkerAlreadyRunning")); |
6 | } |
7 | this.isRunning = true; |
8 | this.cancellationPending = false; |
9 | this.asyncOperation = AsyncOperationManager.CreateOperation(null); |
10 | this.threadStart.BeginInvoke(argument, null, null); |
11 | } |
在threadStart委托中指定的WorkerThreadStart方法将触发DoWork事件,使用者通过注册DoWork事件执行异步代码的操作,从下面的代码可以看出在DoWork事件处理程序中不能访问UI元素的原因。
1 | private void WorkerThreadStart(object argument) |
2 | { |
3 | object result = null; |
4 | Exception error = null; |
5 | bool cancelled = false; |
6 | try |
7 | { |
8 | DoWorkEventArgs e = new DoWorkEventArgs(argument); |
9 | this.OnDoWork(e); |
10 | if (e.Cancel) |
11 | { |
12 | cancelled = true; |
13 | } |
14 | else |
15 | { |
16 | result = e.Result; |
17 | } |
18 | } |
19 | catch (Exception exception2) |
20 | { |
21 | error = exception2; |
22 | } |
23 | RunWorkerCompletedEventArgs arg = new RunWorkerCompletedEventArgs(result, error, cancelled); |
24 | this.asyncOperation.PostOperationCompleted(this.operationCompleted, arg); |
25 | } |
在上述代码中,this.OnDoWork(e)方法产生DoWork事件,DoWork事件处理程序执行完成后会判断在事件处理程序中是否对DoWorkEventArgs.Cancel属性进行了设置,如果使用者调用了CancelAsync 方法那么DoWorkEventArgs.Cancel会被设置为true,事件处理程序正常执行完成时可以从DoWorkEventArgs.Result得到执行结果,如果出现处理异常将扑获异常,所有需要的信息将包含在RunWorkerCompletedEventArgs实例中,最后执行asyncOperation.PostOperationCompleted方法产生RunWorkerCompleted 事件,因此在RunWorkerCompleted事件处理程序中可以获得取消操作、处理异常或处理结果的信息。
类似于RunWorkerCompleted事件的发生机制,对于异步操作进度通知事件发生通过ReportProgress方法实现。
1 | public void ReportProgress(int percentProgress, object userState) |
2 | { |
3 | if (!this.WorkerReportsProgress) |
4 | { |
5 | throw new InvalidOperationException(SR.GetString("BackgroundWorker_WorkerDoesntReportProgress")); |
6 | } |
7 | ProgressChangedEventArgs arg = new ProgressChangedEventArgs(percentProgress, userState); |
8 | if (this.asyncOperation != null) |
9 | { |
10 | this.asyncOperation.Post(this.progressReporter, arg); |
11 | } |
12 | else |
13 | { |
14 | this.progressReporter(arg); |
15 | } |
16 | } |
调用者在DoWork事件处理程序中通过调用ReportProgress方法进行进度汇报,其内部通过asyncOperation.Post方法产生ProgressChanged 事件,如果asyncOperation为null,那么就调用progressReporter方法产生事件,但是调用progressReporter方法产生事件明显存在问题,因为这样产生的事件所在线程同DoWork事件为同一线程,ProgressChanged事件处理程序也会执行在DoWork线程同一上下文中,因此在ProgressChanged事件处理程序中访问ProgressBar控件将出现“线程间操作无效: 从不是创建控件“progressBar1”的线程访问它。”的异常。笔者认为这样的处理是组件的一个Bug,如果asyncOperation为null,更好的处理方式是抛出异常或不做通知处理。值得一提的是,在控制台应用程序中测试调用progressReporter方法不会出现“线程间操作无效”的异常。
结合构造函数,下面的代码有助于进一步理解ProgressChanged事件和RunWorkerCompleted事件产生机制。
1 | public BackgroundWorker() |
2 | { |
3 | this.threadStart = new WorkerThreadStartDelegate(this.WorkerThreadStart); |
4 | this.operationCompleted = new SendOrPostCallback(this.AsyncOperationCompleted); |
5 | this.progressReporter = new SendOrPostCallback(this.ProgressReporter); |
6 | } |
7 | private void ProgressReporter(object arg) |
8 | { |
9 | this.OnProgressChanged((ProgressChangedEventArgs)arg); |
10 | } |
11 | private void AsyncOperationCompleted(object arg) |
12 | { |
13 | this.isRunning = false; |
14 | this.cancellationPending = false; |
15 | this.OnRunWorkerCompleted((RunWorkerCompletedEventArgs)arg); |
16 | } |
最后,看一下RunWorkerAsync方法和CancelAsync方法的实现。
1 | public void RunWorkerAsync(object argument) |
2 | { |
3 | if (this.isRunning) |
4 | { |
5 | throw new InvalidOperationException(SR.GetString("BackgroundWorker_WorkerAlreadyRunning")); |
6 | } |
7 | this.isRunning = true; |
8 | this.cancellationPending = false; |
9 | this.asyncOperation = AsyncOperationManager.CreateOperation(null); |
10 | this.threadStart.BeginInvoke(argument, null, null); |
11 | } |
12 | |
13 | public void CancelAsync() |
14 | { |
15 | if (!this.WorkerSupportsCancellation) |
16 | { |
17 | throw new InvalidOperationException(SR.GetString("BackgroundWorker_WorkerDoesntSupportCancellation")); |
18 | } |
19 | this.cancellationPending = true; |
20 | } |
结束语
BackgroundWorker组件简化了基于事件的异步操作编程,根据其实现原理可进一步编写支持多任务的异步操作组件来更好的满足异步操作密集的应用开发需求。