雁过请留痕...
代码改变世界

研究BackgroundWorker后发现:AsyncOperation和SynchronizationContext的差异真的很大!

2012-08-22 15:58  xiashengwang  阅读(1607)  评论(1编辑  收藏  举报

今天研究BackgroundWorker代码时发现,两处代码的写法有些不一致,于是好奇的测试了一番,以为能测出BackgroundWorker的一个bug。结果大家都知道microsoft胜了。

下面来看看过程,BackgroundWorker类里的ReportProgress方法

public void ReportProgress(int percentProgress, object userState)
{
if (!this.WorkerReportsProgress) throw new InvalidOperationException("BackgroundWorker_WorkerDoesntReportProgress");
ProgressChangedEventArgs arg = new ProgressChangedEventArgs(percentProgress, userState);
if (this.asyncOperation != null)//这里对asyncOperation加了判断
this.asyncOperation.Post(this.progressReporter, arg);
else
this.progressReporter(arg);
}

BackgroundWorker的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);
        }

上面两个方法中,一个判断asyncOperation为null,一个不判断。asyncOperation的创建是在RunWorkerAsync方法中完成的。一直觉得微软的代码都是写得很严谨的,他这样写一定有道理。于是去深入挖掘asyncOperation是如何创建的:

        public void RunWorkerAsync(object argument)
        {
            if (this.isRunning) throw new InvalidOperationException("BackgroundWorker_WorkerAlreadyRunning");
            this.isRunning = true;
            this.cancellationPending = false;
            //创建一个asyncOperation
            this.asyncOperation = AsyncOperationManager.CreateOperation(null);


            this.threadStart.BeginInvoke(argument, null, null);
        }

一般来说,这个方法应该在UI线程中调用,这样asyncOperation中的SynchronizationContext就会是一个WindowsFormsSynchronizationContext,对于更新和完成事件中对于UI控件的更新就不会出问题。但是,如果调用RunWorkerAsync方法的不是UI线程呢?带着这个疑问,我故意启动一个新的线程来初始化一个BackgroundWorker,结果就出现了线程间的非法访问。但是没加判断的那句代码的this.asyncOperation应该为null才对呀,所有虽然是异常了,但不是我预期的NullException异常。于是只好再往下挖掘asyncOperation的创建。

BackgroundWorker内部使用的是AsyncOperation。大家都知道,AsyncOperation是对SynchronizationContext的一个包装。

1,当我们在一个非UI线程上调用SynchronizationContext.Current会返回一个null。

2,当我们在一个非UI线程上调用AsyncOperationManager.CreateOperation创建一个AsyncOperation,是不是也应该返回null呢?

事与愿违,它永远都不会为null,这也就罢了,包装在里面的SynchronizationContext该为null了吧?

再次失望,它还是不为null。

来看看真相吧:

    public static class AsyncOperationManager
    {
        public static AsyncOperation CreateOperation(object userSuppliedState)
        {
            return AsyncOperation.CreateOperation(userSuppliedState, SynchronizationContext);
        }

        [EditorBrowsable(EditorBrowsableState.Advanced)]
        public static System.Threading.SynchronizationContext SynchronizationContext
        {
            get
            {
                //下面这句就是根源,当前线程没有同步上下文,会创建一个新的上下文,晕!
                if (System.Threading.SynchronizationContext.Current == null) System.Threading.SynchronizationContext.SetSynchronizationContext(new System.Threading.SynchronizationContext());
                return System.Threading.SynchronizationContext.Current;
            }
            [PermissionSet(SecurityAction.LinkDemand, Name="FullTrust")]
            set
            {
                System.Threading.SynchronizationContext.SetSynchronizationContext(value);
            }
        }
    }

看看上面的代码就明白了,如果当前线程的同步上下文为null,它就恒定的为工作者线程设置了一个SynchronizationContext,而SynchronizationContext的Post方法只是将代理事件简单的放入线程池,来达到异步的目的。所以真正执行我们的事件回调的线程不是UI线程,当然要报线程间非法访问UI控件的错。

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

总结一下上面所要表达的:

1,上面的BackgroundWorker的代码明显写得不够严谨,两个函数都不用加null判断,误导读者(当然也没人叫我们去反编译微软的代码-_-)。

2,如果用SynchronizationContext就要判断是不是为null,用AsyncOperation就不用判断。

3,对RunWorkerAsync的调用只能放在UI线程中,否则不能将回调封送到UI线程中。

4,弄清楚他们的区别后,你用哪一个都不是问题。