WPF Application至少包含了两个线程,隐藏在后台与DirectX交互的rendering thread和用来处理界面事件、界面绘制和界面逻辑代码的UI thread,大多数应用程序都只是利用这个UI thread,但在处理一些复杂耗时的逻辑代码时将会导致前台界面僵死,如何解决这个问题就是这篇文章的初衷。在里也有一篇文章是解决这个问题的,我只是做些补充和记录。
根据MSDN的官方说法,UI thread将一系列的操作存储到Dispatcher队列中,这从Reflector也可以看到<> ,Dispatcher将会根据优先级来执行队列中的每一项操作,这和原先Win32中的消息泵很类似,WPF应该只是对Win32中消息泵API的封装。
Dispatcher队列的大部分项目无非是处理一个键盘鼠标事件或者一些逻辑代码,但是当遇到一个特耗时的家伙,它跑到远程服务器上抓数据或者溜一个非常耗时的算法,就阻塞了其他与界面响应相关的操作了,用户此时再去点击鼠标、敲键盘想取消全都于事无补,要是程序很顽强,直到这个耗时操作运行完毕,界面又神奇般恢复过来了……
最直接的解决方案就是把这个耗时的方法放到另一个后台线程中运行,而UI thread只处理与用户界面相关的操作,等后台线程运行完毕后把结果交给UI thread来显示。这里需要强调的是,对于WPF中的一个控件,除了其创建线程外的其他线程是不能直接操作该控件的,否则将可能造成多个线程争夺控件资源而产生死锁等问题。
WPF中的大部分类都是继承自DispatcherObject,在实例创建期间,内部会保持一个对其创建线程的Dispatcher的引用,代码执行过程通过调用VerifyAccess方法就可以确认当前线程与创建线程是否一致,这也就是我们在后台线程运行过程中直接调用UI thread上的属性或者方法时碰到异常的源头,其Dispatcher是不一致的。
那我们该如何更新UI thread里的元素呢?前边我们已经说过,UI thread也是通过其Dispatcher队列依次执行操作,那我们只要将更新操作封装到一个delegate里,然后塞到UI thread的Dispatcher队列委托给它就好了。Dispatcher给我们提供了两个方法可以做到,这就是Invoke和BeginInvoke,其不同在于前者是阻塞的同步调用,后者为非阻塞的异步调用。我开始认为这也只是对Win32 API中的SendMessage和PostMessage的封装,不过在阅读了之后,拿Reflector看Dispatcher.Invoke的反编译代码,发现Invoke在调用了的过程中,居然再次调用 这个方法,而BeginInvokeImpl确实是通过来与API交互的,这样就说明了Invoke是通过PostMessage和DispatcherOperation.Wait来完成同步调用的,并非直接通过SendMessage,顿时恍然大悟。
这里要提醒一下,Dispatcher和Delegate的BeginInvoke方法是不同的,他们的名字完全一样是因为目的都是为了异步调用,但是如果创建了某种Delegate的一个实例,然后调用BeginInvoke是从线程池中抓一个线程来异步执行该委托,这样就可以避免阻塞当前线程,而Dispatcher只是把这段代码塞入UI Thread里的队列中按优先级排序后等待执行。其具体区别如下表所列:
执行方式 | 切换到线程 | |
Delegate.Invoke | 同步 | Thread Pool |
Delegate.BeginInvoke | 异步 | Thread Pool |
Dispatcher.Invoke | 同步 | UI Thread |
Dispatcher.BeginInvoke | 异步 | UI Thread |
同时,在调用了Delegate.BeginInvoke之后一定要注意,必须调用EndInvoke来结束这个异步调用过程,否则将可能会造成某种泄漏。
除了这种委托给新线程的解决方案之外,还有没有其他的方式,MSDN上的WPF Sample给出了使用单线程的方式,那就是利用刚刚提到的Dispatcher.BeginInvoke,除了用来被其他线程用来加入UI thread队列之外,UI thread本身也可以通过它来分段多次调用耗时操作的代码,不过前提是必须把这个代码拆分成若干小任务,在UI thread的Dispatcher调配UI任务间隙来处理这些已拆分的小任务。我想MSDN上的这个图能够很简单的说明问题,在处理鼠标事件和UI重绘的间隙不断地运行递归代码。
这种方法的好处就在于我们不用再考虑如何去同步两个线程之间的关系,因为自始至终都在UI thread这一个线程上下文里,不会出现非法操作的异常,不过问题在于,有时候去拆分一个任务不是那么容易,这就必须得用到委托的异步调用、线程(池)或者BackgroundWorker了,关于这些讨论,园子里已经有很多了,这里就不再赘述。