BackgroundWork的内部实现

最近在学习多线程方面的东西,打算在自己的小程序中尝试使用。所以也看了不少文章。关于BackgroundWork,其实去年就用Reflector大概看过,但是没有太懂,呵呵,今天看相关文章正好又碰到,所以就仔细看看。

 

一 异步编程

 

在开始介绍BackgroundWork之前还是废话一下,说说异步编程。异步操作通常用于执行完成时间可能较长的任务,如打开大文件、连接远程计算机或查询数据库。异步操作在主应用程序线程以外的线程中执行。应用程序调用方法异步执行某个操作时,应用程序可在异步方法执行其任务时继续执行。

说到异步编程,就不能不说到多线程,他们之间的关系有时总是让人很迷惑。在学习的初期,你会觉得多线程和异步没有什么区别。我自己开一个新线程去执行一个任务和用异步执行一个任务感觉没什么区别。甚至分不清到底用什么。其实多线程是实现异步编程的一种方式,但不是实现异步编程的唯一方式。

如果学习过计算机体系结构就应该知道,CPU和外设之间通信有三种方式。比如请求打印机,当打印机没有准备好,这个时候就等待,直到打印机准备好,在执行操作,这个叫程序查询方式。但因为外设速度慢,浪费CPU时间;后来出现了硬件中断。在等待打印机的过程中,CPU分配给其他进程,当打印机准备好了,则发一个硬件中断,通知CPU打印。这样有效的提高了CPU的效率,但是频繁的中断和进程的切换,也降低了效率;于是就出现了DAM(Direct Memory Access),DMA最明显的一个特点是它不是用软件而是采用一个专门的控制器来控制内存与外设之间的数据交流,无须CPU介入,大大提高CPU的工作效率。在进行DMA数据传送之前,DMA控制器会向CPU申请总线控制权,CPU如果允许,则将控制权交出,因此,在数据交换时,总线控制权由DMA控制器掌握,在传输结束后,DMA控制器将总线控制权交还给CPU。

所以我们有多种实现异步的方式:利用多线程;IOCP 等,而IOCP就是使用了硬件的功能,虽然我们多起了一个线程,但是线程那I/O请求交给硬件驱动后就空闲了,回到线程池被其他调用。比如读取文件使用异步处理,需要把流修改为异步,并且调用Read的异步方法,这时候,线程把请求交给I/O就被回收了,流返回后在使用一个线程进行处理;如果只调用 Read异步方法,而流不支持异步,实际上是在内部用一个线程模拟了所谓的异步;最后就是流使支持异步,但使用同步的read方法,这个时候线程采用轮询的方法,等待操作完成。

 

二 .NET中的异步编程模型

 

 

在.NET 中有三种异步编程的方式,使用最多的是APM,我们可以看到,很多类都提供了BeginXXX,EndXXX这样的方法,就是异步方法。另外一种方法就是委托实现异步编程,委托提供了BeginInvoke、EndInvoke方法实现异步编程。他们有3种聚集的方式,分别是:等待完成、轮询和会回调。而最后一种异步编程模型就是基于事件驱动的,也就是我们今天要具体看的BackgroundWork控件,他就是一个基于事件驱动的复杂异步编程控件。

关于异步编程模型: http://msdn.microsoft.com/zh-cn/library/ms228969.aspx     ,MSND上的<异步编程设计模式>介绍的很详细。是比较好的文章。

 

三 BackgroundWork

 

 

前面提到了,BackgroundWork是基于事件驱动的,所以使用起来要比APM方便使用过的应该知道,我们只需要只需要绑定3个事件,就能很轻松的完成复杂的异步操作,并且可以轻松获取执行的进度、取消执行等功能。下面就具体看看他的内部实现。为我们实现自己的异步功能也有所帮助。

 

 

BackgroundWork代码结构

backgroundwork

代码中定义了DoWork、ProgressChanged、RunWorkerCompleted这三个事件,我们在使用时也只用把方法注册到时间上就行了。我们知道定义一个事件的同时,还会有一个引发该事件的protect virtual方法,在上面代码中就是OnDoWork、OnProgressChanged、OnRunWorkerCompleted这3个受保护的虚方法。他们的实现都很简单就是触发事件,下面是OnDoWork的实现,其他2个完全一样;

        protected virtual void OnDoWork(DoWorkEventArgs e)
        {
            DoWorkEventHandler handler = (DoWorkEventHandler) base.Events[doWorkKey];
            if (handler != null)
            {
                handler(this, e);
            }
        }

接着看其他代码,里面还有RunWorkerAsync、ProgressReporter、ReportProgress、WorkerThreadStart等方法,看名字就明白,主要是启动和进度报告用。而私有字段中则定义了AsyncOperation对象和SendOrPostCallback委托,这2个是比较特殊也比较重要的,在后面在说。

 

 

客户端使用BackgroundWork

 

这里我并不打算介绍具体如何使用这个控件,而是我们在使用时的顺序:

  1. 给BackgroundWork的3个事件绑定处理的代码;(WORK方法、WORKING方法、WORKDOWN方法)
  2. 调用RunWorkerAsync 方法,开始执行异步操作
  3. 在DoWork事件绑定的方法中调用ReportProgress ,以便触发ProgressChanged事件,而执行ProgressChanged绑定的方法
  4. 执行完成后会触发RunWorkerCompleted事件,执行与之相绑定的方法。

注意到上面黑体的2个方法是我调用的方法,也是唯一提供的2个公有方法。整个过程也很好理解,我们要做的操作很少,而他的思路和APM中的回调方式比较相似,但是他相比起来更为灵活,因为一个事件可以挂在多个方法,而且在运行时可以实时返回进度。知道了他的过程,我们就来看看他具体是如何实现的。

 

内部实现

 

在看具体代码前,先看看类中定义的变量和委托,具体的意义已经写出来了,接下来就看看具体代码了

// 工作线程启动委托
private delegate void WorkerThreadStartDelegate(object argument);
// 标识状态的变量
private bool isRunning;                 // 是否在执行
private bool canCancelWorker;           // 能否取消执行
private bool cancellationPending;       // 是否已请求取消后台操作   
private bool workerReportsProgress;     // 是否报告进度
private AsyncOperation asyncOperation;  // 区分不同的异步任务
// 表示在消息即将被调度到同步上下文时要执行的回调方法
private readonly SendOrPostCallback operationCompleted; 
private readonly SendOrPostCallback progressReporter;
// 工作线程启动委托的方法
private readonly WorkerThreadStartDelegate threadStart;
// 触发事件是使用的对象
private static readonly object doWorkKey = new object();
private static readonly object progressChangedKey = new object();
private static readonly object runWorkerCompletedKey = new object();

构造方法:构造方法里面很简单,初始化了私有变量中的3个委托对象;他们都是3个私有的方法

public BackgroundWorker()
{
    this.threadStart = new WorkerThreadStartDelegate(this.WorkerThreadStart);
    this.operationCompleted = new SendOrPostCallback(this.AsyncOperationCompleted);
    this.progressReporter = new SendOrPostCallback(this.ProgressReporter);
}

我们先来看看ProgressReporterAsyncOperationCompleted 这2个私有方法的代码:是不是也很简单,就是调用触发了事件方法。

        private void AsyncOperationCompleted(object arg)
        {
            this.isRunning = false;
            this.cancellationPending = false;
            this.OnRunWorkerCompleted((RunWorkerCompletedEventArgs) arg);
        }
        private void ProgressReporter(object arg)
        {
            this.OnProgressChanged((ProgressChangedEventArgs) arg);
        }

看完了初始化,以及委托相关的方法,在客户端我们的程序就要启动了,这个时候我们调用RunWorkerAsync 方法

        public void RunWorkerAsync(object argument)
        {
            if (this.isRunning)
            {
                throw new InvalidOperationException(SR.GetString("BackgroundWorker_WorkerAlreadyRunning"));
            }
            this.isRunning = true;
            this.cancellationPending = false;
            this.asyncOperation = AsyncOperationManager.CreateOperation(null);
            this.threadStart.BeginInvoke(argument, null, null);
        }

代码中首先判断是否在执行,如果已经执行则抛出一个异常,也就是说我们的BackgroundWork不支持并发处理,所以我们在自己代码中,调用RunWorkerAsync 方法之前,应该判断IsBusy 属性(它返回的就是isRunning 而不是让系统抛出异常然后去捕获。接下来就是设置字段状态。然后实例化了asyncOperation 对象,这个对象后面在介绍。最主要的就是最后一句,threadStart.BeginInvoke 这里是我们前面提到的 利用委托实现异步,这个也是BackgroundWork异步的内部实现。委托的BeginInvoke是开启一个新的线程执行,这里执行的委托就是在构造函数中设置的WorkerThreadStart 方法

        private void WorkerThreadStart(object argument)
        {
            object result = null;
            Exception error = null;
            bool cancelled = false;
            try
            {
                DoWorkEventArgs e = new DoWorkEventArgs(argument);
                this.OnDoWork(e);
                if (e.Cancel)
                {
                    cancelled = true;
                }
                else
                {
                    result = e.Result;
                }
            }
            catch (Exception exception2)
            {
                error = exception2;
            }
            RunWorkerCompletedEventArgs arg = new RunWorkerCompletedEventArgs(result, error, cancelled);
            this.asyncOperation.PostOperationCompleted(this.operationCompleted, arg);
        }

代码中的this.OnDoWork(e); 触发了DoWork事件绑定的方法,也就是我们需要异步操作的方法。因为这个方法是在新的线程中执行,而不是在UI线程,这也就是为什么我们不能在自己的WORK方法中操作界面控件。(为什么不能?因为VS2005开始强制不允许非控件创建线程修改控件。当然这还是有办法的,具体可以参见:http://www.cnblogs.com/yizhu2000/archive/2008/01/03/1011958.html#wm1 这里介绍了在非UI线程修改控件的办法,不过当你看完本文,你会发现BackgroundWork用了一种不可思议的方式来实现)。

这个时候我们的WORK方法开始执行了,而UI线程可以执行其他操作,比如重绘,界面也不在会假死。因为我们在WORK方法中调用了ReportProgress 方法来触发ProgressChanged事件,以便我们的WORKING方法能够被执行。下面就看看ReportProgress 方法

        public void ReportProgress(int percentProgress, object userState)
        {
            if (!this.WorkerReportsProgress)
            {
                throw new InvalidOperationException(SR.GetString("BackgroundWorker_WorkerDoesntReportProgress"));
            }
            ProgressChangedEventArgs arg = new ProgressChangedEventArgs(percentProgress, userState);
            if (this.asyncOperation != null)
            {
                this.asyncOperation.Post(this.progressReporter, arg);
            }
            else
            {
                this.progressReporter(arg);
            }
        }

首先检查是否能够报告工作进度,如果不行,会跑出异常,所以如果要使用他,我们要把控件的WorkerReportsProgress属性设置为True。接下来的代码有些难以理解。主要是asyncOperation对象。MSDN给出的定义是:根据基于事件的异步模式概述实现类时,可能需要跟踪在类的实例上调用的每个异步操作的生存期。AsyncOperation 类提供了多种方法来跟踪和报告异步任务的进度。若要将进度和中间结果报告给客户端,请从异步辅助代码调用 Post。若要指示异步任务已经完成,或要取消挂起的异步任务,请调用 PostOperationCompleted。

但是在执行之前,先判断了asyncOperation是否为null,如果不为null,调用psot方法;如果为空怎直接执行progressReporter委托。 但是我们可以发现,这样的话有问题,因为执行委托方法是在非UI线程,如果WORKING事件中有操作UI控件就会抛出异常。而post方法很特殊,他是把当前方法发送到UI线程上执行。但是这里好像不会出现asyncOperation为空的情况,不过不为空应该抛出异常或什么处理也不做比较合适。

这里的post方法的一个参数是SendOrPostCallback 委托,另一个就是要传递的对象,而参数progressReporter我们前面已经知道了,她是指向了触发ProgressReporter方法,来触发ProgressChanged事件。所以我们的WORKING实际上是通过:WORK方法–>ReportProgress方法–>(post方法)–>progressReporter委托 –>ProgressReporter方法–>OnProgressChanged方法–>ProgressChanged事件–>WORKING方法

 

我们在WORK中每调用一次ReportProgress方法,就会执行一次我们自定义的WORKING方法,或许你会问了,那为什么不直接在WORK中调用WORKING,虽然这样是可以,但是还是那个问题,非UI线程不能修改UI控件,所以中间转了这么一道,progressReporter委托 –>ProgressReporter 这一步就是为了利用了post方法。而第2个参数在整个过程中起到了传递数据的作用。

当WORK方法执行完成后,获取result对象,而最后调用了asyncOperation 对象的PostOperationCompleted 方法。他也的参数也是一个SendOrPostCallback 委托,指向了AsyncOperationCompleted方法,方法代码如下:

        private void AsyncOperationCompleted(object arg)
        {
            this.isRunning = false;
            this.cancellationPending = false;
            this.OnRunWorkerCompleted((RunWorkerCompletedEventArgs) arg);
        }

他的过程是:WORK方法执行完–>PostOperationCompleted 方法–>operationCompleted委托– >AsyncOperationCompleted方法–>OnRunWorkerCompleted方法–>RunWorkerCompleted事件–>WORKDOWN方法 可以发现他和前面的过程是一样的,这里离就不过多解释了。

 

 取消执行

 

要取消正在执行的任务时,可以调用CancelAsync方法:

        public void CancelAsync()
        {
            if (!this.WorkerSupportsCancellation)
            {
                throw new InvalidOperationException(SR.GetString("BackgroundWorker_WorkerDoesntSupportCancellation"));
            }
            this.cancellationPending = true;
        }

代码也是会去检查是否支持取消执行,所以如果我们允许取消,就要在使用前把WorkerSupportsCancellation设置为true。否则会抛出异常。

 

AsyncOperation对象

 

看完了BackgroundWorker的代码,发现其实挺简单的,代码也很少,他的核心思想就是:基于事件的异步模型。我们只需要关联3个事件就能完成一部操作。因为工作方法在非UI线程上执行,为了操作UI控件,利用了post等方法,以一种不可思议的形式,把执行的方法从当前线程发送到了UI线程,而这也是 BackgroundWorker的核心,而AsyncOperation 对象就是核心。所以我们可以利用他来建立我们自己的异步控件。

asyncOperation

上面是AsyncOperation类, 其中Post和PostOperationCompleted就是我们用到的方法,他们有相同的参数

public void Post(SendOrPostCallback d, object arg)
{
    this.VerifyNotCompleted();
    this.VerifyDelegateNotNull(d);
    this.syncContext.Post(d, arg);
}
 
public void PostOperationCompleted(SendOrPostCallback d, object arg)
{
    this.Post(d, arg);
    this.OperationCompletedCore();
}

发现,PostOperationCompleted和post一样,只是最后执行了一个操作完成的方法。在Post方法中,前2个方法是验证异步调用是否已经执行完以及SendOrPostCallback委托是否绑定了方法(如果没有,我们前台方法无法执行,就失去了意义)。

public virtual void Post(SendOrPostCallback d, object state)
{
    ThreadPool.QueueUserWorkItem(new WaitCallback(d.Invoke), state);
}

 

在看看syncContext.Post(d , arg )方法的实现,她是调用了SynchronizationContext对象的post方法,SynchronizationContext类MSDN的解释是:此类实现的同步模型的目的是使公共语言运行库内部的异步/同步操作能够针对不同的异步模型采取正确的行为。此模型还简化了托管应用程序为在不同的同步环境下正常工作而必须遵循的一些要求。

也正式他是的我们非UI线程上的方法,可以采用正确的行为,那就是使用UI线程操作。而Post方法是将异步消息调度到一个同步上下文。 按道理,是发送到UI线程上,但是在代码中看到是用的是使用了线程池中的一个线程。这里的行为以及POST到底是如何工作的就很让人费解了。但是注意,他是一个虚方法,可能被重写,那么我们看看syncContext 是怎么创建的。首先syncContext 是一个asyncOperation对象内部维护的一个字段。那么我们就要看看asyncOperation对象创建的情况,还记得前面this.asyncOperation = AsyncOperationManager.CreateOperation(null); 的代码,他是一个AsyncOperationManager对象来创建的。我们看看这个类

 

[HostProtection(SecurityAction.LinkDemand, SharedState=true)]
// AsyncOperationManager类
public static class AsyncOperationManager
{
    // Methods
    public static AsyncOperation CreateOperation(object userSuppliedState)
    {
        return AsyncOperation.CreateOperation(userSuppliedState, SynchronizationContext);
    }
    // Properties
    [EditorBrowsable(EditorBrowsableState.Advanced)]
    public static SynchronizationContext SynchronizationContext
    {
        get
        {
            if (SynchronizationContext.Current == null)
            {
                SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());
            }
            return SynchronizationContext.Current;
        }
        [PermissionSet(SecurityAction.LinkDemand, Name="FullTrust")]
        set
        {
            SynchronizationContext.SetSynchronizationContext(value);
        }
    }
}
 
// AsyncOperation类的创建
internal static AsyncOperation CreateOperation(object userSuppliedState, SynchronizationContext syncContext)
{
    return new AsyncOperation(userSuppliedState, syncContext);
}

 

这是他的全部代码,他实际还是利用AsyncOperation对象自己的CreateOperation方法,其中第2个参数就是一个同步上下文对象,而它正是AsyncOperationManager对象的SynchronizationContext 属性。 这个属性返回的是SynchronizationContext.Current 。

 

public static SynchronizationContext Current
{
    get
    {
        ExecutionContext executionContextNoCreate = Thread.CurrentThread.GetExecutionContextNoCreate();
        if (executionContextNoCreate != null)
        {
            return executionContextNoCreate.SynchronizationContext;
        }
        return null;
    }
}

看上面代码,SynchronizationContext.Current 是通过Thread.CurrentThread.GetExecutionContextNoCreate得到的一个同步上下文,进入到源码可以看到Get方法返回了 ExecutionContext上下文中的同步上下文字段。ExecutionContext类为与执行的逻辑线程相关的所有信息提供单个容器。这包括安全上下文、调用上下文、同步上下文、本地化上下文和事务上下文。

 

从SynchronizationContext的层次结构可以看到,他有一个子类,名为:WindowsFormsSynchronizationContext , 为提供 Windows 窗体应用程序模型的同步上下文。当我们创建第一个控件时,系统会为这个UI线程附加上一个上下文,当然也就包括了我们这里用到同步上下文。

而BackgroundWork在DoWork中获得的了UI线程上的同步上下文,而且是WindowsFormsSynchronizationContext 类型的同步上下文,最重要的是此类型重写了SynchronizationContext的Post方法:

public override void Post(SendOrPostCallback d, object state)
{
    if (this.controlToSendTo != null)
    {
        this.controlToSendTo.BeginInvoke(d, new object[] { state });
    }
}

他调用了一个controlToSendTo 变量的BeginInvoke方法,在去看看这个变量 private Control controlToSendTo ;

哈哈,怎么样,前面还认为不可思议的东西,原来也还是回到了老路上,利用了控件的BeginInvoke方法来更新界面,而这恰恰是非UI线程更新UI控件的一种方法。(具体参见:http://www.cnblogs.com/worldreason/archive/2008/06/09/1216127.html

所以这里实际是利用同步上下文实现了线程之间的通信。而BackgroundWork是在程序开始时保存UI线程的同步上下文,并且此上下文重写了Post方法,利用Control.BeginInvoke把代理方法最终发到UI线程了执行。 (SynchronizationContext详细的解释参见:http://www.codeproject.com/KB/threads/SynchronizationContext.aspx )

2010.7.2 更新SynchronizationContext部分


如果本文对您有帮助,可以扫描下方二维码打赏!您的支持是我的动力!
微信打赏 支付宝打赏

2 评论

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注