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來封裝這些執行緒級的操作。