前言:
這一段時間開始在着手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