前言:
这一段时间开始在着手WPF的项目,在开发过程的间歇恶补下WPF基础。asyc await作为framework4.5的新特性,也在我的项目中得到应用。有个这个特性以后确实又是一个大大的语法糖福利,程序代码漂亮简洁多。大致的执行顺序也可以从院子的一篇「async & await的前世今生」得知,微软msdn的例子也是简洁明了,但是总有一种“知其然而不知所以然”的感觉,萦绕在心头很是难受。微软给我们封装得太好了,让我们即使“不求甚解”的情况下也可以玩得转。就像是骇客帝国,不甘心与再在这个“盒子”中安逸了,至少开一扇天窗来一探究竟。
正文:
既然这段时间一直在补WPF的基础,那么对await的研究也打算从这个框架入手。
首先先写一个最最简单的WPFDemo来作为研究对象。
<Window x:Class="WPFResearchDispatcher.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="350" Width="525"> <Grid> <StackPanel> <Rectangle x:Name="rectangle" Height="200" ></Rectangle> <Button x:Name="btnTest" Click="btnTest_Click" Height="100">Click</Button> </StackPanel> </Grid> </Window>
xmal上无非就是一个矩形和一个测试按钮
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading; 6 using System.Threading.Tasks; 7 using System.Windows; 8 using System.Windows.Controls; 9 using System.Windows.Data; 10 using System.Windows.Documents; 11 using System.Windows.Input; 12 using System.Windows.Media; 13 using System.Windows.Media.Imaging; 14 using System.Windows.Navigation; 15 using System.Windows.Shapes; 16 using System.Windows.Threading; 17 18 namespace WPFResearchDispatcher 19 { 20 /// <summary> 21 /// MainWindow.xaml 的交互逻辑 22 /// </summary> 23 public partial class MainWindow : Window 24 { 25 public MainWindow() 26 { 27 InitializeComponent(); 28 } 29 30 private async void btnTest_Click(object sender, RoutedEventArgs e) 31 { 32 var task = Task.Delay(2000); 33 await task; 34 35 this.rectangle.Fill = Brushes.Red; 36 37 } 38 } 39 }
这是一个普通的不能再普通的await的Sample,点击按钮后2秒,矩形填充色变成红色。UI不会卡顿。of course,it works.
接下来,我们分别在btnTest_Click,开始时,和await之后分别加入断点,看看发生了什么。
点击Click按钮
断点命中,让我们再来看看调用堆栈
这时候大家可能觉得很奇怪,说到这堆栈还和await没半毛钱的关系。大家先不要急,让我先慢慢说完再慢慢细细回味。
这个调用堆栈很明确勾勒出了点击事件的链路。
WPF的底层还是以Windows消息队列来实现的。
开启程序后,主程序的Dispatcher, PushFrame开启消息泵。
鼠标外设点击后,在操作系统层面以WindowsAPI的消息队列发出一个消息。
WPF获取此消息后,InputManger找到这个区域的相关控件来RaiseEvent路由事件,最后到达我们的btnTest_Click方法。
对于这一过程的消息过程,周永恒的一站式WPF--Window(一)里面有很深入的介绍,这这里就不搬书了。
这个Windows消息的值,通过调试窗口,我们可以获得是514,先记下来以后有用。
继续F5,等了2秒中后继续命中断点,这个断点是在await以后了
让我们再来看调用堆栈
我们发现这次调用堆栈比前面要短了很多。
我们发现也是从SubclassWndProc接收Windows消息队列开始的。中间的InputManger的处理,路由事件什么的统统不见了,也就是说,除了执行的方法体是在btnTest_Click里面,其实这段程序的执行和前面的Click事件完全没有任何关系。
为了进一步证实推断,我再次查看这个windows消息的id,为49563,和前面的完全是两个消息。是Dispatcher分别处理的。
调试到这一步,冒出了更多的疑问,那么这个windows消息的id 49563是哪里来的拿。第一次消息是操作系统发的。那么第二次消息是哪个家伙“偷偷”发了,而我们还不知道。
进一步跟进代码,到Dispatcher类里面去找些蛛丝马迹。
我的办法比较笨和死板,一层层的看下去。终于在 WindowsBase.dll!MS.Win32.HwndWrapper.WndProc(System.IntPtr hwnd, int msg, System.IntPtr wParam, System.IntPtr lParam, ref bool handled) 行 235 C#
这个调用堆栈找到线索
private IntPtr WndProcHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) { WindowMessage message = (WindowMessage) msg; if (this._disableProcessingCount > 0) { throw new InvalidOperationException(MS.Internal.WindowsBase.SR.Get("DispatcherProcessingDisabledButStillPumping")); } if (message == WindowMessage.WM_DESTROY) { if (!this._hasShutdownStarted && !this._hasShutdownFinished) { this.ShutdownImpl(); } } else if (message == _msgProcessQueue) { this.ProcessQueue(); }
这个函数全部代码较长,为了突出重点,我这里就只取前面部分,通过堆栈我们可以知道下个调用函数是ProcessQueue()
通过调试窗口,我看到 _msgProcessQueue正好是49563!
好了再回来看_msgProcessQueue是什么
[SecurityCritical] private static WindowMessage _msgProcessQueue = MS.Win32.UnsafeNativeMethods.RegisterWindowMessage("DispatcherProcessQueue");
这是定义在Dispatcher类中的静态自定义注册消息
那么有出必然是有进啊,那这消息是谁“推”进来的
我再看看_msgProcessQueue有什么引用,是private的找起来很方便,在整个Dispatcher
再找到RequestProcessing
再找到InvokeAsyncImpl
LegacyBeginInvokeImpl
越来越近了。。。。
兜兜转转绕一圈,最后发现是BeginInvoke。欧!好像有点感觉了!
正好msdn有篇文章Await, SynchronizationContext, and Console Apps。
里面论述了Await是因为SynchronizationContext的关系,SynchronizationContext是抽象类。我们可以很容易获知WPF的SynchronizationContext的实现是DispatcherSynchronizationContext。
摘录原文如下,作者用代码示意了await的实现。
await FooAsync(); RestOfMethod(); as being similar in nature to this: var t = FooAsync(); var currentContext = SynchronizationContext.Current; t.ContinueWith(delegate { if (currentContext == null) RestOfMethod(); else currentContext.Post(delegate { RestOfMethod(); }, null); }, TaskScheduler.Current);
实际上是在完成异步方法后“偷偷”调用了 SynchronizationContext的Post的方法。
趁热打铁,马上杀回到DispatcherSynchronizationContext里面去把这代码扣出来
public override void Post(SendOrPostCallback d, object state) { this._dispatcher.BeginInvoke(this._priority, d, state); }
代码无比的简洁,和调试代码的结论一致,这个“环”终于开始圆起来了。
好,最后为了证实自己的推论,最后再做一次验证。
在WindowsBase.dll引用下Enable Debugging
在Post方法上加入断点。
再次运行调试
OK,命中,和预期一致,这个断点在我前面设的两个断点中间触发了
再来看看调用堆栈
OK,偷偷摸摸做的事情总算完全曝光在我们眼前了,堆栈的内容也很容易理解。
内部时钟在检测到异步任务完成后开始了PostAcition,把后续要做的事情包装成一个代理,通过BeginInvoke,压到消息泵里。
接下来就是我们第三个断点跟到的,Dispatcher又从队列里拿到了这个message,再开始做await后面的事情……
总结:
await的实现完美的契合在WPF的Dispatcher体系下运转。现在一切一切的解释都显得合理了。
WPF在处理await时,执行到await以后就直接返回了。从Dispatcher这层来讲,方法体执行到这里已经吧这个“消息”下要处理的事情干完了。
这里就可以解释,我们的UI没有任何的死锁。
具体的asyc方法管他自己执行,和消息队列暂时没有任何交集。
asyc方法执行完成后,再获得当前的SynchronizationContext去Post消息,“申请”执行剩余的方法
Dispatcher收到队列消息,执行剩余的方法。
举一个生活化的例子:
小明去ATM机存钱,但是到ATM才刚刚反应过来小明的钱在小李那里,小李要半小时才能赶过来给他。
怎么办了,如果占着ATM的队伍等钱拿来,那只能占着茅坑让其他人埋怨(锁死UI线程)。比较好的办法是小明到了银行后先去干点其他事情(完成返回),让小李早点送钱来(asyc)。小李把钱送来了,小明可以去存钱了,当然他不能直接去存,如果有人在排队的话,他必须开始排队(SynchronizationContext.Post)。小明终于再次等到开始存钱(MessageProc)
其他一些相关的问题:
如果await不使用的化,可以改成await Task.Delay(2000).ConfigureAwait(false);
如果用调试看,堆栈非常简单,是直接线程池的默认实现。和Dispatcher没有任何关系,当然接下来操作UI元素也会报错。。。。
这一段尝试是 await之后的线程问题这篇Blog得到启发。
写在最后
在相关技术资料(Blog)和强悍工具(Refector)的帮助下,总算搞懂了一些一直困扰自己的问题。希望能对大家有所帮助。我的基础实力还是比较浅的,对于Windows编程只有大学时代学的WindowsAPI。对于MFC和C++一些底层都没有好好接触过。如有错误之处欢迎大家批判指正。
相关链接
Await, SynchronizationContext, and Console Apps