深入理解WPF框架下await的实践

前言:

这一段时间开始在着手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之后分别加入断点,看看发生了什么。

深入理解WPF框架下await的实践

点击Click按钮

深入理解WPF框架下await的实践

断点命中,让我们再来看看调用堆栈

深入理解WPF框架下await的实践

深入理解WPF框架下await的实践

这时候大家可能觉得很奇怪,说到这堆栈还和await没半毛钱的关系。大家先不要急,让我先慢慢说完再慢慢细细回味。

 

这个调用堆栈很明确勾勒出了点击事件的链路。

WPF的底层还是以Windows消息队列来实现的。

开启程序后,主程序的Dispatcher, PushFrame开启消息泵。

鼠标外设点击后,在操作系统层面以WindowsAPI的消息队列发出一个消息。

WPF获取此消息后,InputManger找到这个区域的相关控件来RaiseEvent路由事件,最后到达我们的btnTest_Click方法。

对于这一过程的消息过程,周永恒的一站式WPF--Window(一)里面有很深入的介绍,这这里就不搬书了。

这个Windows消息的值,通过调试窗口,我们可以获得是514,先记下来以后有用。

继续F5,等了2秒中后继续命中断点,这个断点是在await以后了

深入理解WPF框架下await的实践

让我们再来看调用堆栈

深入理解WPF框架下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);
        }

代码无比的简洁,和调试代码的结论一致,这个“环”终于开始圆起来了。

好,最后为了证实自己的推论,最后再做一次验证。

深入理解WPF框架下await的实践

在WindowsBase.dll引用下Enable Debugging

深入理解WPF框架下await的实践

在Post方法上加入断点。

深入理解WPF框架下await的实践

再次运行调试

OK,命中,和预期一致,这个断点在我前面设的两个断点中间触发了

再来看看调用堆栈

深入理解WPF框架下await的实践

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++一些底层都没有好好接触过。如有错误之处欢迎大家批判指正。

 

相关链接

周永恒Blog

async & await 的前世今生

Await, SynchronizationContext, and Console Apps

 

深入理解WPF框架下await的实践

上一篇:CTF misc 第十五天


下一篇:delphi7 在虚拟机 vbox里面安装失败