深入WPF -- Dispatcher(補)

 

作者:周永恆
出處:http://www.cnblogs.com/Zhouyongh  

本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。

書接前文,前篇文章介紹了WPF中的Dispatcher,由於概念太多,可能不是那麼好理解。這篇文章繼續討論,希望在線程和Dispatcher這個點上,能把它講透。

從哪說起?

  按照慣例,在深入問題之前,先找一個插入點,希望這個插入點能爲朋友們所理解。

  新建一個Window程序,代碼如下:

int WINAPI _tWinMain(HINSTANCE hInstance,  HINSTANCE hPrevInstance,
                     LPTSTR    lpCmdLine,  int       nCmdShow)
{
    RegisterWindowClass(hInstance);                                          //1 

    HWND hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,        //2
        CW_USEDEFAULT, 0, CW_USEDEFAULT,z 0, NULL, NULL, hInstance, NULL);
    ShowWindow(hWnd, nCmdShow);

    MSG msg;                                                                     //3
    while (GetMessage(&msg, NULL, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return (int) msg.wParam;
}

  其中的RegisterWindowClass

WORD RegisterWindowClass(HINSTANCE hInstance)
{
    WNDCLASSEX wcex;
    wcex.lpszClassName  = szWindowClass;
    wcex.lpfnWndProc    = WndProc;
    ...
}
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
    case WM_PAINT:
    ...
}

  這個創建窗口並顯示的過程如下:

  1. 調用RegisterWindowClass註冊窗口類,關聯其中的窗口過程WndProc。
  2. 調用CreateWindow創建窗口並顯示。
  3. (主線程)進入GetMessage循環,取得消息後調用DispatchMessage分發消息。

  這裏的GetMessage循環就是所謂的消息泵,它像水泵一樣源源不斷的從線程的消息隊列中取得消息,然後調用DispatchMessage把消息分發到各個窗口,交給窗口的WndProc去處理。

  用一副圖來表示這個過程:

_thumb7

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


  1. 鼠標點擊。
  2. 操作系統底層獲知這次點擊動作,根據點擊位置遍歷找到對應的Hwnd,構建一個Window消息MSG,把這個消息加入到創建該Hwnd線程的消息隊列中去。
  3. 應用程序主線程處於GetMessage循環中,每次調用GetMessage獲取一個消息,如果線程的消息隊列爲空,則線程會被掛起,直到線程消息隊列存在消息線程會被重新激活。
  4. 調用DispatchMessage分發消息MSG,MSG持有一個Hwnd的字段,指明瞭消息應該發往的Hwnd,操作系統在第2步構建MSG時會設置這個值。
  5. 消息被髮往Hwnd,操作系統回調該Hwnd對應的窗口過程WndProc,由WndProc來處理這個消息。

  這是一個簡略的Window消息處理流程,往具體說這個故事會很長,讓我們把目光收回到WPF,看看WPF和即將介紹的Dispatcher在這個基礎上都做了些什麼,又有哪些出彩的地方。

仍然從Main函數說起

  作爲應用程序的入口點,我們仍然從Main函數走進WPF。

  新建一個WPF工程,如下:

_thumb21

 

 

 

 

 

  默認的WPF工程中中是找不到傳統的Program.cs文件的,它的App.xaml文件的編譯動作爲ApplicationDefinition,編譯後,編譯器會自動生成App.g.cs文件,包含了Main函數。如下:

        [System.STAThreadAttribute()]
        [System.Diagnostics.DebuggerNonUserCodeAttribute()]
        public static void Main() 
    {
            WpfApplication3.App app = new WpfApplication3.App();
            app.InitializeComponent();
            app.Run();
        }

  這裏出現了Application類,按MSDN上的解釋,“Application 是一個類,其中封裝了 WPF 應用程序特有的功能,包括:應用程序生存期;應用程序範圍的窗口、屬性和資源管理;命令行參數和退出代碼處理;導航”等。

  調用app.Run()之後,按照前面Win32的步驟,應用程序應進入到一個GetMessage的消息泵之中,那麼對WPF程序來說,這個消息泵是什麼樣的呢?又和Dispatcher有什麼關係呢?

走進Dispatcher

  Dispatcher的構造函數是私有的,調用Dispacher.CurrentDispatcher會獲得當前線程的Dispatcher,Dispatcher內部持有一個靜態的所有Dispatcher的List。因爲構造函數私有,只能調用CurrentDispatcher來獲得Dispatcher,可以保證對同一個線程,只能創建一個Dispatcher。

  Dispatcher提供了一個Run函數,來啓動消息泵,內部的核心代碼是我們所熟悉的,如:

        while (frame.Continue)
        {
            if (!GetMessage(ref msg, IntPtr.Zero, 0, 0))
                break;
            TranslateAndDispatchMessage(ref msg);
        }

  這裏出現了一個Frame的概念,暫且不談,來看看Dispatcher相對於傳統的消息循環,有哪些改進的地方。

Dispatcher的新意

  在Winform的消息循環中,

  1. 爲了線程安全,調用Control的Invoke或者BeginInvoke方法可以在創建控件的線程上執行委託,方法的返回值分別爲object和IAsyncResult。儘管可以使用IAsyncResult的IsCompleted和AsyncWaitHandle等方法來輪詢或者等待委託的執行,但對於對任務的控制來講,這個粒度是不夠的,我們不能取消(Cancel)一個已經調用BeginInvoke的委託任務,也不能更換兩個BeginInvoke的執行順序。
  2. 更爲友好的接口支持,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:

  1. DispatcherOperation是如何被創建的。
  2. DispatcherOperation是何時被執行的。
  3. DispatcherOperation是怎樣被執行的。

  Dispatcher提供了BeginInvoke和Invoke兩個方法,其中BeginInvoke的返回值是DispatcherOperation,Invoke函數的內部調用了BeginInvoke,也就是說,DispatcherOperation就是在這兩個函數中被創建出來的。我們可以調用這兩個函數創建新的DO,WPF內部也調用了這兩個函數,把Window消息轉化爲DispatcherOperation,用一副圖表示如下:

_thumb2

 

 

 

 

 

 

 

 

  1. 窗口過程WndProc接收到Window消息,調用Dispatcher的Invoke方法,創建一個DispatcherOperation。Dispatcher內部持有一個DispatcherOperation的隊列,用來存放所有創建出來的DispatcherOperation。默認一個DO被創建出來後,會加入到這個隊列中去。WndProc調用Invoke的時候比較特殊,他傳遞的優先級DispatcherPriority爲Send,這是一個特殊的優先級,在Invoke時傳遞Send優先級WPF會直接執行這個DO,而不把它加入到隊列中去。
  2. 用戶也可以隨時調用Invoke或者BeginInvoke方法加入新的DO,在DispatcherOperation處理的時候也可能會調用BeginInvoke加入新的DO。

  DO被加入到Dispatcher的隊列中去,那麼這個隊列又是何時被處理呢?Dispatcher在創建的時候,創建了一個隱藏的Window,在DO加入到隊列後,Dispatcher會向自己的隱藏Window發送一個自定義的Window消息(DispatcherProcessQueue)。當收到這個消息後,會按照優先級和隊列順序取出第一個DO並執行:

  1. 用戶調用BeginInvoke。
  2. Dispatcher創建了一個DO,加入到DO隊列中去,並向自己的隱藏窗口Post自定義消息(DispatcherProcessQueue)。
  3. 創建隱藏窗口時會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  

本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章