一、Dispatcher介紹
微軟在WPF引入了Dispatcher,那么這個Dispatcher的主要作用是什么呢?
不管是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來封裝這些線程級的操作。
文章列表