1. 程式人生 > 實用技巧 >WPF執行緒詳解之(一)——Dispatcher詳解

WPF執行緒詳解之(一)——Dispatcher詳解

我的理解:

Dispatcher是執行緒排程管理器,用在子執行緒重新整理主執行緒(UI執行緒)(比如繫結的時候,屬性更新的時候),在子執行緒裡面起一個dispatcher,將工作專案排程到 UI 執行緒,讓主執行緒排程重新整理UI的程式碼。

不管是WinForm應用程式還是WPF應用程式,實際上都是一個程序,一個程序可以包含多個執行緒,其中有一個是主執行緒,其餘的是子執行緒。在WPF或WinForm應用程式中,主執行緒負責接收輸入、處理事件、繪製螢幕等工作,為了使主執行緒及時響應,防止假死,在開發過程中對一些耗時的操作、消耗資源比較多的操作,都會去建立一個或多個子執行緒去完成操作,比如大資料量的迴圈操作、後臺下載。這樣一來,由於UI介面是主執行緒建立的,所以子執行緒不能直接更新由主執行緒維護的UI介面。

Dispatcher的作用是用於管理執行緒工作項佇列,類似於Win32中的訊息佇列,Dispatcher的內部函式,仍然呼叫了傳統的建立視窗類,建立視窗,建立訊息泵等操作。Dispatcher本身是一個單例模式,建構函式私有,暴露了一個靜態的CurrentDispatcher方法用於獲得當前執行緒的Dispatcher。對於執行緒來說,它對Dispatcher是一無所知的,Dispatcher內部維護了一個靜態的 List<Dispatcher> _dispatchers, 每當使用CurrentDispatcher方法時,它會在這個_dispatchers中遍歷,如果沒有找到,則建立一個新的Dispatcher對 象,加入到_dispatchers中去。Dispatcher內部維護了一個Thread的屬性,建立Dispatcher時會把當前執行緒賦值給這個 Thread的屬性,下次遍歷查詢的時候就使用這個欄位來匹配是否在_dispatchers中已經儲存了當前執行緒的Dispatcher。

二、Dispatcher的繼承關係

在 WPF 的類層次結構中,大部分都集中派生於 DispatcherObject 類(通過其他類)。如下圖所示,您可以看到 DispatcherObject 虛擬類正好位於 Object 下方和大多數 WPF 類的層次結構之間。 要了解他們之間的關係可以參看下面這張類繼承關係圖:

對上圖的一些說明:

1) System.Object 類:大家都知道在.Net中所有型別的基類,DispatcherObject 就繼承於它,所以它是WPF的基類。

2) System.Windows.Threading.DispatcherObject 類:從圖中看WPF 中的使用到的大部分控制元件與其他類大多是繼承 DispatcherObject 類,它提供了用於處理併發和執行緒的基本構造。

3) System.Windows.DependencyObject類:對WPF中的依賴項屬性承載支援與 附加屬性承載支援,表示參與 依賴項屬性 系統的物件。

4) System.Windows.Media.Visual類:為 WPF 中的呈現提供支援,其中包括命中測試、座標轉換和邊界框計算等。

5) System.Windows.UIElement 類:UIElement 是 WPF 核心級實現的基類,該類是 Windows Presentation Foundation (WPF) 中具有可視外觀並可以處理基本輸入的大多數物件的基類。

6) System.Windows.FrameworkElement類:為 Windows Presentation Foundation (WPF) 元素提供 WPF 框架級屬性集、事件集和方法集。此類表示附帶的 WPF 框架級實現,它是基於由UIElement定義的 WPF 核心級 API 構建的。

7) System.Windows.Controls.Control 類:表示 使用者介面 (UI) 元素的基類,這些元素使用 ControlTemplate 來定義其外觀。

8) System.Windows.Controls.ContentControl類:表示沒有任何型別的內容表示單個控制元件。

WPF的絕大部分的控制元件,還包括視窗本身都是繼承自ContentControl的。

ContentControl族包含的控制元件

Button

ButtonBase

CheckBox

ComboBoxItem

ContentControl

Frame                    

GridViewColumnHeader

GroupItem

Label

ListBoxItem

ListViewItem

NavigationWindow

RadioButton

RepeatButton

ScrollViewer

StatusBarItem

ToggleButton

ToolTip

UserControl

Window

9) System.Windows.Controls.ItemsControl 類:表示可用於提供專案的集合的控制元件。

以條目集合位內容的控制元件ItemsControl

特點: a.均派生自ItemsControl

b.內容屬性為Items或ItemsSource

c.每種ItemsControl都對應有自己的條目容器(Item Container).

ItemsControl族包含的控制元件

Menu                                                 

MenuBase

ContextMenu

ComboBox

ItemsControl

ListBox

ListView

TabControl

TreeView

Selector

StatusBar

10) System.Windows.Controls.Panel類:為所有 Panel 元素提供基類。 使用 Panel 元素定位和排列在 Windows Presentation Foundation (WPF) 應用程式的子物件。

11)System.Windows.Sharps.Sharp類:為 Ellipse、Polygon 和 Rectangle 之類的形狀元素提供基類。

三、走進Dispatcher

所有 WPF 應用程式啟動時都會載入兩個重要的執行緒:一個用於呈現使用者介面,另一個用於管理使用者介面。呈現執行緒是一個在後臺執行的隱藏執行緒,因此您通常面對的唯一執行緒 就是 UI 執行緒。WPF 要求將其大多數物件與 UI 執行緒進行關聯。這稱之為執行緒關聯,意味著要使用一個 WPF 物件,只能在建立它的執行緒上使用。在其他執行緒上使用它會導致引發執行時異常。 UI 執行緒的作用是用於接收輸入、處理事件、繪製螢幕以及執行應用程式程式碼。

在 WPF 中絕大部分控制元件都繼承自 DispatcherObject,甚至包括 Application。這些繼承自 DispatcherObject 的物件具有執行緒關聯特徵,也就意味著只有建立這些物件例項,且包含了 Dispatcher 的執行緒(通常指預設 UI 執行緒)才能直接對其進行更新操作。

DispatcherObject 類有兩個主要職責:提供對物件所關聯的當前 Dispatcher 的訪問許可權,以及提供方法以檢查 (CheckAccess) 和驗證 (VerifyAccess) 某個執行緒是否有權訪問物件(派生於 DispatcherObject)。CheckAccess 與 VerifyAccess 的區別在於 CheckAccess 返回一個布林值,表示當前執行緒是否可以使用物件,而 VerifyAccess 則線上程無權訪問物件的情況下引發異常。通過提供這些基本的功能,所有 WPF 物件都支援對是否可在特定執行緒(特別是 UI 執行緒)上使用它們加以確定。如下圖。

在 WPF 中,DispatcherObject 只能通過與它關聯的 Dispatcher 進行訪問。 例如,後臺執行緒不能更新由 UI 執行緒建立的 Label的內容。

那麼如何更新UI執行緒建立的物件資訊呢?Dispatcher提供了兩個方法,Invoke和BeginInvoke,這兩個方法還有多個不同引數的過載。其中Invoke內部還是呼叫了BeginInvoke,一個典型的BeginInvoke引數如下:

   

 public DispatcherOperation BeginInvoke(Delegate method, DispatcherPriority priority, params object[] args); 

Invoke 是同步操作,而 BeginInvoke 是非同步操作。 該這兩個操作將按指定的 DispatcherPriority 新增到 Dispatcher 的佇列中。  DispatcherPriority定義了很多優先順序,可以分為前臺優先順序和後臺優先順序,其中前臺包括 Loaded~Send,後臺包括Background~Input。剩下的幾個優先順序除了Invalid和Inactive都屬於空閒優先順序。這個前臺優先順序和後臺優先順序的分界線是以Input來區分的,這裡的Input指的是鍵盤輸入和滑鼠移動、點選等等。

DispatchPriority 優先級別

優先順序

說明

Invalid

這是一個無效的優先順序。

Inactive

工作專案已排隊但未處理。

SystemIdle

僅當系統空閒時才將工作專案排程到 UI 執行緒。這是實際得到處理的專案的最低優先順序。

ApplicationIdle

僅當應用程式本身空閒時才將工作專案排程到 UI 執行緒。

ContextIdle

僅在優先順序更高的工作專案得到處理後才將工作專案排程到 UI 執行緒。

Background

在所有佈局、呈現和輸入專案都得到處理後才將工作專案排程到 UI 執行緒。

Input

以與使用者輸入相同的優先順序將工作專案排程到 UI 執行緒。

Loaded

在所有佈局和呈現都完成後才將工作專案排程到 UI 執行緒。

Render

以與呈現引擎相同的優先順序將工作專案排程到 UI 執行緒。

DataBind

以與資料繫結相同的優先順序將工作專案排程到 UI 執行緒。

Normal

以正常優先順序將工作專案排程到 UI 執行緒。這是排程大多數應用程式工作專案時的優先順序。

Send

以最高優先順序將工作專案排程到 UI 執行緒。

四、使用Dispatcher

下面我們來用一個例項,來看看如何正確從一個非 UI 執行緒中更新一個由UI執行緒建立的物件。

1、錯誤的更新方式

XAML程式碼:

<Window x:Class="WpfApp1.WindowThd"

        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

        Title="WindowThd" Height="300" Width="400">

    <Grid>

 

        <StackPanel>

            <Label x:Name="lblHello">歡迎你光臨WPF的世界!</Label>

            <Button Name="btnThd" Click="btnThd_Click" >多執行緒同步呼叫</Button>

            <Button Name="btnAppBeginInvoke" Click="btnAppBeginInvoke_Click" >BeginInvoke 非同步呼叫</Button>

        </StackPanel>

    </Grid>

 

</Window>

 

後臺程式碼:

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Threading;

using System.Threading.Tasks;

using System.Windows;

using System.Windows.Controls;

using System.Windows.Data;

using System.Windows.Documents;

using System.Windows.Input;

using System.Windows.Media;

using System.Windows.Media.Imaging;

using System.Windows.Shapes;

 

namespace WpfApp1

{

    /// <summary>

    /// WindowThd.xaml 的互動邏輯

    /// </summary>

    public partial class WindowThd : Window

    {

        public WindowThd()

        {

            InitializeComponent();

     

     

    }

 

    private void ModifyUI()

    {

        // 模擬一些工作正在進行

        Thread.Sleep(TimeSpan.FromSeconds(2));

        lblHello.Content = "歡迎你光臨WPF的世界,Dispatcher";

    }

 

    private void btnThd_Click(object sender, RoutedEventArgs e)

    {

        Thread thread = new Thread(ModifyUI);

        thread.Start();

    }

 

    }

}


錯誤截圖:

2、正確的更新方式,從上例中我們看到了從子執行緒中直接更新UI執行緒建立的物件,會報錯。應該如何修改呢?我們把上面的程式碼修改成如下,再來看看會是什麼效果。

private void ModifyUI()

    {

        // 模擬一些工作正在進行

        Thread.Sleep(TimeSpan.FromSeconds(2));

        //lblHello.Content = "歡迎你光臨WPF的世界,Dispatcher";

        this.Dispatcher.Invoke(DispatcherPriority.Normal, (ThreadStart)delegate()

        {

            lblHello.Content = "歡迎你光臨WPF的世界,Dispatche  同步方法 !!";

        });

}


當然Dispatcher類也提供了BeginInvoke方法,我們也可以使用如下程式碼,來完成對Lable的Content的更新。

private void btnAppBeginInvoke_Click(object sender, RoutedEventArgs e)

    {

               new Thread(() =>

        {

            Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Normal,

                new Action(() =>

                {

                    Thread.Sleep(TimeSpan.FromSeconds(2));

                    this.lblHello.Content = "歡迎你光臨WPF的世界,Dispatche 非同步方法!!"+ DateTime.Now.ToString();

                }));

        }).Start();

    }

 

五、小結

  在WPF中,所有的WPF物件都派生自DispatcherObject,DispatcherObject暴露了Dispatcher屬性用來取得建立 物件執行緒對應的Dispatcher。DispatcherObject物件只能被建立它的執行緒所訪問,其他執行緒修改 DispatcherObject需要取得對應的Dispatcher,呼叫Invoke或者BeginInvoke來投入任務。Dispatcher的一些設計思路包括 Invoke和BeginInvoke等從WinForm時代就是一直存在的,只是使用了Dispatcher來封裝這些執行緒級的操作。