朱燚

--书到读透处,酒于微醺时

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::
  80 随笔 :: 22 文章 :: 585 评论 :: 46 引用

公告

上篇文章介绍了多种线程的创建方式,以及winform在多线程编程时的特殊性,这篇我们来介绍一下异步编程的经典模式和微软对其的实现


微软推荐的异步操作模型是事件模型,也即用子线程通过事件来通知调用者自己的工作状态,也就是设计模式中的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解决了封送的问题,让我们的回调事件代码逻辑简单清晰,推荐大家使用.

posted on 2007-10-19 09:48 朱燚:-) 阅读(...) 评论(...) 编辑 收藏