一、BackgroundWorker组件经验之谈
在VS2005中添加了BackgroundWorker组件,该组件在多线程编程方面使用起来非常方便,然而在开始时由于没有搞清楚它的使用机制,走了不少的弯路,现在把我在使用它的过程中的经验与诸位分享一下。
BackgroundWorker类中主要用到的有这列属性、方法和事件:
重要属性:
1、CancellationPending获取一个值,指示应用程序是否已请求取消后台操作。通过在DoWork事件中判断CancellationPending属性可以认定是否需要取消后台操作(也就是结束线程);
2、IsBusy获取一个值,指示 BackgroundWorker 是否正在运行异步操作。程序中使用IsBusy属性用来确定后台操作是否正在使用中;
3、WorkerReportsProgress获取或设置一个值,该值指示BackgroundWorker能否报告进度更新
4、WorkerSupportsCancellation获取或设置一个值,该值指示 BackgroundWorker 是否支持异步取消。设置WorkerSupportsCancellation为true使得程序可以调用CancelAsync方法提交终止挂起的后台操作的请求;
重要方法:
1、CancelAsync请求取消挂起的后台操作
2、RunWorkerAsync开始执行后台操作
3、ReportProgress引发ProgressChanged事件
重要事件:
1、DoWork调用 RunWorkerAsync 时发生
2、ProgressChanged调用 ReportProgress 时发生
3、RunWorkerCompleted当后台操作已完成、被取消或引发异常时发生
另外还有三个重要的参数是RunWorkerCompletedEventArgs以及DoWorkEventArgs、ProgressChangedEventArgs。
BackgroundWorker的各属性、方法、事件的调用机制和顺序:
从上图可见在整个生活周期内发生了3次重要的参数传递过程:
参数传递1:此次的参数传递是将RunWorkerAsync(Object)中的Object传递到DoWork事件的DoWorkEventArgs.Argument,由于在这里只有一个参数可以传递,所以在实际应用往封装一个类,将整个实例化的类作为RunWorkerAsync的Object传递到DoWorkEventArgs.Argument;
参数传递2:此次是将程序运行进度传递给ProgressChanged事件,实际使用中往往使用给方法和事件更新进度条或者日志信息;
参数传递3:在DoWork事件结束之前,将后台线程产生的结果数据赋给DoWorkEventArgs.Result一边在RunWorkerCompleted事件中调用RunWorkerCompletedEventArgs.Result属性取得后台线程产生的结果。
另外从上图可以看到DoWork事件是在后台线程中运行的,所以在该事件中不能够操作用户界面的内容,如果需要更新用户界面,可以使用ProgressChanged事件及RunWorkCompleted事件来实现。
明白了BagkgroundWorker的事件调用顺序和参数传递机制之后在使用该组件用于多线程编程的时候就可以轻松许多了。
二、异步编程的经典模式和微软对其的实现
微软推荐的异步操作模型是事件模型,也即用子线程通过事件来通知调用者自己的工作状态,也就是设计模式中的observer模式,也可以看成是上文中线程类的扩展,最后实现后调用效果类似于
MyThread thread=new MyThread()
thread.Work+=new ThreadWork(Calculate)
thread.WorkComplete+=new WorkComplete(DisplayResult)
Calculate(object sender, EventArgs e)){
....
}
DisplayResult(object sender, EventArgs e)){
...
}
<例一>
这个话题已经有许多很好的文章,大家参考http://www.cnblogs.com/net66/archive/2005/08/03/206132.html,其作者在文章后附加有示例项目,项目中的线程类实现了事件发送,线程终止,报告任务进度等一系列必要的功能,大家可以自己去查看代码,我就不赘述了,我主要谈微软对这个模式的实现BackgroundWorker
上篇文章里说到了控制权的问题,上面的模型在winform下使用有个问题就是执行上下文的问题,在回调函数中(比如<例一>中的DisplayResult中),我们不得不使用BeginInvoke,才能调用ui线程创建的控件的属性和方法,
比如在上面net66的例子里
//创建线程对象
_Task = new newasynchui();
//挂接进度条修改事件
_Task.TaskProgressChanged += new TaskEventHandler( OnTaskProgressChanged1 );
//在UI线程,负责更新进度条
private void OnTaskProgressChanged1( object sender,TaskEventArgs e )
{
if (InvokeRequired ) //不在UI线程上,异步调用
{
TaskEventHandler TPChanged1 = new TaskEventHandler( OnTaskProgressChanged1 );
this.BeginInvoke(TPChanged1,new object[] {sender,e});
Console.WriteLine("InvokeRequired=true");
}
else
{
progressBar.Value = e.Progress;
}
}
<例二>
可以看到,在函数里面用到了
if(InvokeRequired)
{...BeginInvoke....}
else
{....}
这个模式来保证方法在多线程和单线程下都可以运行,所以线程逻辑和界面逻辑混合在了一起,以至把以前很简单的只需要一句话的任务:progressBar.Value = e.Progress;搞的很复杂,如果线程类作为公共库来提供,对编写事件的人要求会相对较高,那么有什么更好的办法呢?
其实在.Net2.0中微软自己实现这个模式,制作了Backgroundworker这个类,他可以解决上面这些问题,我们先来看看他的使用方法
System.ComponentModel.BackgroundWorker bw = new System.ComponentModel.BackgroundWorker();
//定义需要在子线程中干的事情
bw.DoWork += new System.ComponentModel.DoWorkEventHandler(bw_DoWork);
//定义执行完毕后需要做的事情
bw.RunWorkerCompleted += new System.ComponentModel.RunWorkerCompletedEventHandler(bw_RunWorkerCompleted);
//开始执行
bw.RunWorkerAsync();
static void bw_RunWorkerCompleted(object sender, System.ComponentModel.RunWorkerCompletedEventArgs e)
{
MessageBox.Show("Complete"+Thread.CurrentThread.ManagedThreadId.ToString());
}
static void bw_DoWork(object sender, System.ComponentModel.DoWorkEventArgs e)
{
MessageBox.Show(Thread.CurrentThread.ManagedThreadId);
}
<例三>
注意我在两个函数中输出了当前线程的ID,当我们在WindowsForm程序中执行上述代码时,我们惊奇的发现,bw_RunWorkerCompleted这个回调函数居然是运行在UI线程中的,也就是说在这个方法中我们不用再使用Invoke和BeginInvoke调用winform中的控件了, 更让我奇怪的是,如果是在ConsoleApplication中同样运行这段代码,那么bw_RunWorkerCompleted输出的线程id和主线程id就并不相同.
那么BackgroundWorker到底是怎么实现跨线程封送的呢?
阅读一下这个类的代码,我们发现他借助了AsyncOperation.Post(SendOrPostCallback d, object arg)
在winform下使用这个函数,就可以使得由SendOrPostCallback定义被封送会UI线程,聪明的博友可以用这个方法来实现自己的BackgroundWorker.
继续查看下去,发现关键在于AsyncOperation的syncContext字段,这是一个SynchronizationContext类型的对象,而这个对象的Post方法具体实现了封送,当我继续查看
SynchronizationContext.Post方法时,里面简单的令人难以执行
public virtual void Post(SendOrPostCallback d, object state)
{
ThreadPool.QueueUserWorkItem(new WaitCallback(d.Invoke), state);
}
这是怎么回事情呢,线程池本省并不具备线程封送的能力啊
联想到在Winform程序和Console程序下程序的行为是不同的,而且SynchronizationContext的Post方法是一个virtual方法,我猜测这个方法可能被继承自他的类重写了
查询Msdn,果然发现在这个类有两个子类,其中一个就是WindowsFormsSynchronizationContext,我们来看看这个类的Post方法
public override void Post(SendOrPostCallback d, object state)
{
if (this.controlToSendTo != null)
{
this.controlToSendTo.BeginInvoke(d, new object[] { state });
}
}
哈哈,又是熟悉的beginInvoke,原来控制台程序和Winform程序加载的SynchronizationContext是不同的,所以行为才有所不同,通过简单的测试,我们可以看到控制台程序直接使用基类(SynchronizationContext),而winform程序使用这个WindowsFormsSynchronizationContext的Post方法把方法调用封送到控件的线程.
总结:
同时这个类还提供了进度改变事件,允许用户终止线程,功能全面,内部使用了线程池,能在一定成都上避免了大量线程的资源耗用问题,并通过SynchronizationContext解决了封送的问题,让我们的回调事件代码逻辑简单清晰,推荐大家使用.