深入理解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之后分別加入斷點,看看發生了什么。

點擊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++一些底層都沒有好好接觸過。如有錯誤之處歡迎大家批判指正。

 

相關鏈接

周永恆Blog

async & await 的前世今生

Await, SynchronizationContext, and Console Apps

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM