今天研究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,弄清楚他们的区别后,你用哪一个都不是问题。