作者:周永恆
出處:http://www.cnblogs.com/Zhouyongh
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。
書接前文,前篇文章介紹了WPF中的Dispatcher,由於概念太多,可能不是那麼好理解。這篇文章繼續討論,希望在線程和Dispatcher這個點上,能把它講透。從哪說起?
按照慣例,在深入問題之前,先找一個插入點,希望這個插入點能爲朋友們所理解。
新建一個Window程序,代碼如下:
|
其中的RegisterWindowClass
|
這個創建窗口並顯示的過程如下:
- 調用RegisterWindowClass註冊窗口類,關聯其中的窗口過程WndProc。
- 調用CreateWindow創建窗口並顯示。
- (主線程)進入GetMessage循環,取得消息後調用DispatchMessage分發消息。
這裏的GetMessage循環就是所謂的消息泵,它像水泵一樣源源不斷的從線程的消息隊列中取得消息,然後調用DispatchMessage把消息分發到各個窗口,交給窗口的WndProc去處理。
用一副圖來表示這個過程:
- 鼠標點擊。
- 操作系統底層獲知這次點擊動作,根據點擊位置遍歷找到對應的Hwnd,構建一個Window消息MSG,把這個消息加入到創建該Hwnd線程的消息隊列中去。
- 應用程序主線程處於GetMessage循環中,每次調用GetMessage獲取一個消息,如果線程的消息隊列爲空,則線程會被掛起,直到線程消息隊列存在消息線程會被重新激活。
- 調用DispatchMessage分發消息MSG,MSG持有一個Hwnd的字段,指明瞭消息應該發往的Hwnd,操作系統在第2步構建MSG時會設置這個值。
- 消息被髮往Hwnd,操作系統回調該Hwnd對應的窗口過程WndProc,由WndProc來處理這個消息。
這是一個簡略的Window消息處理流程,往具體說這個故事會很長,讓我們把目光收回到WPF,看看WPF和即將介紹的Dispatcher在這個基礎上都做了些什麼,又有哪些出彩的地方。
仍然從Main函數說起
作爲應用程序的入口點,我們仍然從Main函數走進WPF。
新建一個WPF工程,如下:
默認的WPF工程中中是找不到傳統的Program.cs文件的,它的App.xaml文件的編譯動作爲ApplicationDefinition,編譯後,編譯器會自動生成App.g.cs文件,包含了Main函數。如下:
|
這裏出現了Application類,按MSDN上的解釋,“Application 是一個類,其中封裝了 WPF 應用程序特有的功能,包括:應用程序生存期;應用程序範圍的窗口、屬性和資源管理;命令行參數和退出代碼處理;導航”等。
調用app.Run()之後,按照前面Win32的步驟,應用程序應進入到一個GetMessage的消息泵之中,那麼對WPF程序來說,這個消息泵是什麼樣的呢?又和Dispatcher有什麼關係呢?
走進Dispatcher
Dispatcher的構造函數是私有的,調用Dispacher.CurrentDispatcher會獲得當前線程的Dispatcher,Dispatcher內部持有一個靜態的所有Dispatcher的List。因爲構造函數私有,只能調用CurrentDispatcher來獲得Dispatcher,可以保證對同一個線程,只能創建一個Dispatcher。
Dispatcher提供了一個Run函數,來啓動消息泵,內部的核心代碼是我們所熟悉的,如:
|
這裏出現了一個Frame的概念,暫且不談,來看看Dispatcher相對於傳統的消息循環,有哪些改進的地方。
Dispatcher的新意
在Winform的消息循環中,
- 爲了線程安全,調用Control的Invoke或者BeginInvoke方法可以在創建控件的線程上執行委託,方法的返回值分別爲object和IAsyncResult。儘管可以使用IAsyncResult的IsCompleted和AsyncWaitHandle等方法來輪詢或者等待委託的執行,但對於對任務的控制來講,這個粒度是不夠的,我們不能取消(Cancel)一個已經調用BeginInvoke的委託任務,也不能更換兩個BeginInvoke的執行順序。
- 更爲友好的接口支持,Windows編程中,在窗口消息循環中加入Hook是常見的需求,Dispatcher提供了DispatcherHooks類,以Event的形式對外提供了OperationAborted,OperationCompleted,OperationPosted等事件。
這裏的Operation指的是DispatcherOperation,爲了更好的控制消息循環,WPF引入了DispatcherOperation來封裝Window消息,這個DispatcherOperation如下:
DispatcherOpration
public sealed class DispatcherOperation { public Dispatcher Dispatcher { get; } public DispatcherPriority Priority { get; set; } public object Result { get; } public DispatcherOperationStatus Status { get; } public event EventHandler Aborted; public event EventHandler Completed; public bool Abort(); public DispatcherOperationStatus Wait();
public DispatcherOperationStatus Wait(TimeSpan timeout);
} |
DispatcherOperation類看起來還是比較簡單明瞭的,以屬性的形式暴露了Result(結果),Status(狀態),以及用事件來指出這個Operation何時結束或者取消。其中比較有意思的是Priority屬性,從字面來看,它表示了DispatcherOperation的優先級,而且提供了get和set方法,也就是說,這個DispatcherOperation是可以在運行時更改優先級的。那麼這個優先級是怎麼回事,Dispatcher又是如何處理DispatcherOperation的呢,讓我們深入DispatcherOperation,來看看它是如何被處理的。
深入DispatcherOperation(DO)
所謂深入,也要有的放矢,從三個方面來談一下DispatcherOperation:
- DispatcherOperation是如何被創建的。
- DispatcherOperation是何時被執行的。
- DispatcherOperation是怎樣被執行的。
Dispatcher提供了BeginInvoke和Invoke兩個方法,其中BeginInvoke的返回值是DispatcherOperation,Invoke函數的內部調用了BeginInvoke,也就是說,DispatcherOperation就是在這兩個函數中被創建出來的。我們可以調用這兩個函數創建新的DO,WPF內部也調用了這兩個函數,把Window消息轉化爲DispatcherOperation,用一副圖表示如下:
- 窗口過程WndProc接收到Window消息,調用Dispatcher的Invoke方法,創建一個DispatcherOperation。Dispatcher內部持有一個DispatcherOperation的隊列,用來存放所有創建出來的DispatcherOperation。默認一個DO被創建出來後,會加入到這個隊列中去。WndProc調用Invoke的時候比較特殊,他傳遞的優先級DispatcherPriority爲Send,這是一個特殊的優先級,在Invoke時傳遞Send優先級WPF會直接執行這個DO,而不把它加入到隊列中去。
- 用戶也可以隨時調用Invoke或者BeginInvoke方法加入新的DO,在DispatcherOperation處理的時候也可能會調用BeginInvoke加入新的DO。
DO被加入到Dispatcher的隊列中去,那麼這個隊列又是何時被處理呢?Dispatcher在創建的時候,創建了一個隱藏的Window,在DO加入到隊列後,Dispatcher會向自己的隱藏Window發送一個自定義的Window消息(DispatcherProcessQueue)。當收到這個消息後,會按照優先級和隊列順序取出第一個DO並執行:
- 用戶調用BeginInvoke。
- Dispatcher創建了一個DO,加入到DO隊列中去,並向自己的隱藏窗口Post自定義消息(DispatcherProcessQueue)。
- 創建隱藏窗口時會Hook它的消息,當收到的消息爲DispatcherProcessQueue時,按照優先級取出隊列中的一個DO,並執行。
每加入一個DO就會申請處理DO隊列,在DO的優先級(DispatcherPriority)被改變的時候也會處理DO隊列,DO在創建時聲明瞭自己的優先級,這個優先級會影響到隊列的處理順序。
DispatcherTimer
鑑於線程親緣性,當需要創建Timer並訪問UI對象時,多使用DispatcherTimer。DispatcherTimer的一個簡單用法如下:
var dispatcherTimer = new System.Windows.Threading.DispatcherTimer(); dispatcherTimer.Tick += new EventHandler(dispatcherTimer_Tick);
dispatcherTimer.Interval = new TimeSpan(0,0,1);
dispatcherTimer.Start();
|
在DispatcherTimer的內部,Timer的Tick事件處理也被包裝成了DispatcherOperation,並調用BeginInvoke加入到Dispatcher中去。當這個DO被執行後,如果DispatcherTimer的狀態仍然爲Enable,DispatcherTimer會繼續調用BeginInvoke加入新的DO。關於Timer的時間處理,Dispatcher會向自己的隱藏窗口調用SetTimer並計算時間間隔,當然,因爲DispatcherOperation有優先級,不能保狀正好在時間間隔時執行這個DO,這個執行的時間會比預計時間偏後而不會超前。
UI線程和Dispatcher
通常,WPF啓動時具有兩個線程,一個處理呈現(Render),另一個用於管理UI。關於Render線程,請參見前文。這個管理UI的線程通常被稱爲UI線程。在WPF中,所有UI對象的基類爲DispatcherObject,WPF在對所有DispatcherObject屬性操作前進行了線程親緣性校驗,只有在創建UI對象的線程中纔可以訪問該UI對象。
前面提到,由於Dispatcher構造函數私有,一個線程最多只能有一個Dispatcher。對UI線程來說,Dispatcher的主要作用就是對任務項(DispatcherOperation)進行排隊。對UI對象來說,DispatcherObject有一個Dispatcher屬性,可以獲得創建該UI對象線程的Dispatcher。這種設計通過Dispatcher統一了UI對象的操作,從使用上隔離了UI對象和線程間的關係。
多線程
多線程操作簡單分爲兩種:多工作線程和多UI線程,當然,也可以有多工作多UI線程,思路是一樣的,省去不談。
程序啓動時默認的主線程就是UI線程,它在調用Application.Run(也就是Dispatcher.Run)之後進入了一個GetMessage的循環中,對Window消息進行響應並構建執行一個個的DispatcherOperation。默認對UI對象的操作都是在這個主線程中,如果進行耗時很長的操作就會造成UI線程長時間不能繼續響應Window消息,造成界面假死等一些的UI響應問題。對這種耗時較長的操作一般需要工作線程來幫忙,操作結束後再通過Dispatcher把結果Invoke到UI線程,如:
TextBlock textBlock = new TextBlock() { Text = "1" }; Thread thread = new Thread(new ThreadStart(() => { //做一些耗時操作,這裏用線程休眠10秒來模擬
Thread.Sleep(TimeSpan.FromSeconds(10)); textBlock.Dispatcher.Invoke(new Action(() =>
{ textBlock.Text = "2";
})); })); thread.Start(); |
當然,除了新建工作線程,也可以使用BackgroundWorker或者線程池中線程來進行耗時操作,操作結束後需要調用UI對象Dispatcher的Invoke或者BeginInvoke方法來操作UI,否則會拋出InvalidOperationException來提示不可跨線程訪問UI對象。
這種多工作線程是很常見的,一般我們討論的多線程大多指這種多工作線程單一UI線程,那麼如何創建多UI線程的程序呢?
多UI線程
在談多UI線程之前,先說說多UI線程使用的場景:
大多數情況下,我們是不需要多UI線程的,所謂多UI線程,就是指有兩個或者兩個以上的線程創建了UI對象。這種做法的好處是兩個UI線程會分別進入各自的GetMessage循環,如果是需要多個監視實時數據的UI,或者說使用了DirectShow一些事件密集的程序,可以考慮新創建一個UI線程(GetMessage循環)來減輕單一消息泵的壓力。當然,這樣做的壞處也很多,不同UI線程中的UI對象互相訪問是需要進行Invoke通信的,爲了解決這個問題,WPF提供了VisualTarget來用於跨線程將一個對象樹連接到另一個對象樹,如:
public class VisualHost : FrameworkElement { public Visual Child
{ get { return _child; } set
{ if (_child != null) RemoveVisualChild(_child); _child = value;
if (_child != null) AddVisualChild(_child); } } protected override Visual GetVisualChild(int index) { if (_child != null && index == 0) return _child;
else
throw new ArgumentOutOfRangeException("index"); } protected override int VisualChildrenCount { get { return _child != null ? 1 : 0; } } private Visual _child;
} |
在另一個UI線程下的VisualTarget:
Window win = new Window();
win.Loaded += (s, ex) => { VisualHost vh = new VisualHost(); HostVisual hostVisual = new HostVisual();
vh.Child = hostVisual; win.Content = vh; Thread thread = new Thread(new ThreadStart(() => { VisualTarget visualTarget = new VisualTarget(hostVisual);
DrawingVisual dv = new DrawingVisual();
using (var dc = dv.RenderOpen())
{ dc.DrawText(new FormattedText("UI from another UI thread", System.Globalization.CultureInfo.GetCultureInfo("en-us"),
FlowDirection.LeftToRight, new Typeface("Verdana"), 32, Brushes.Black), new Point(10, 0));
} visualTarget.RootVisual = dv; Dispatcher.Run(); //啓動Dispatcher })); thread.SetApartmentState(ApartmentState.STA); thread.IsBackground = true;
thread.Start(); }; win.Show();
|
當然,這個多UI線程只是爲了更好的捋清線程間的關係,實際使用中的例子並不是很常見。
總結
Dispatcher是WPF中很重要的一個概念,WPF所有UI對象都是運行在Dispatcher上的。Dispatcher的一些設計思路包括Invoke和BeginInvoke等從WinForm時代就是一直存在的,只是使用了Dispatcher來封裝這些線程級的操作。剩餘一些概念包括跨線程通信的Freezable以及詳細的優先級順序DispatcherPriority本文都沒有細談,以後的文章中會逐步介紹,希望大家多多支持,謝謝。
作者:周永恆
出處:http://www.cnblogs.com/Zhouyongh
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。