溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

WPF線程模型和Dispatcher怎么用

發(fā)布時(shí)間:2021-12-27 09:24:59 來源:億速云 閱讀:184 作者:小新 欄目:編程語言

這篇文章將為大家詳細(xì)講解有關(guān)WPF線程模型和Dispatcher怎么用,小編覺得挺實(shí)用的,因此分享給大家做個(gè)參考,希望大家閱讀完這篇文章后可以有所收獲。

WPF線程模型是從WPF的兩個(gè)線程:一個(gè)用于處理呈現(xiàn)和一個(gè)用于管理UI開始。并展開同時(shí)討論Dispatcher的相關(guān)對(duì)象。

開始著手寫這個(gè)WPF系列,這里的一站式,就是力爭在每一個(gè)點(diǎn)上能把它講透,當(dāng)然,做不到那么盡善盡美,如果有不對(duì)的地方也歡迎朋友們指正,我會(huì)逐步補(bǔ)充,爭取把這個(gè)系列寫好。

通常,WPF應(yīng)用程序從兩個(gè)線程開始:一個(gè)用于處理呈現(xiàn),一個(gè)用于管理UI。呈現(xiàn)線程有效地隱藏在后臺(tái)運(yùn)行,而 UI 線程則接收輸入、處理事件、繪制屏幕以及運(yùn)行應(yīng)用程序代碼。

UI 線程對(duì)一個(gè)名為Dispatcher的對(duì)象內(nèi)的工作項(xiàng)進(jìn)行排隊(duì)。Dispatcher基于優(yōu)先級(jí)選擇工作項(xiàng),并運(yùn)行每一個(gè)工作項(xiàng),直到完成。每個(gè) UI 線程都必須至少有一個(gè)Dispatcher,并且每個(gè)Dispatcher都只能在一個(gè)線程中執(zhí)行工作項(xiàng)。

這兩段是MSDN上關(guān)于WPF線程模型的描述。主要介紹了兩個(gè)概念:一,WPF中線程一分為二,一個(gè)用于呈現(xiàn)(Render),一個(gè)用于管理UI;二,在UI線程中,使用了一個(gè)名為Dispatcher的類幫助UI線程處理任務(wù)。

那么這個(gè)線程模型和Dispatcher到底是怎樣的呢,它又有什么特點(diǎn),有什么優(yōu)缺點(diǎn)呢?在正式分析線程模型和Dispatcher之前,我先找一個(gè)插入點(diǎn),希望這個(gè)插入點(diǎn)能為朋友們所理解。

作為一個(gè)Presentation的基架,WPF的使命就是要編寫圖形化的操作界面。而在Windows操作系統(tǒng)上,圖形化界面是建立在消息機(jī)制這個(gè)基礎(chǔ)上的,那么創(chuàng)建一個(gè)窗口,要經(jīng)歷哪些步驟呢?

1. 創(chuàng)建窗口類。 WNDCLASSEX wcex; RegisterClassEx(&wcex);

2. 創(chuàng)建窗口。CreateWindow(…); ShowWindow(…); UpdateWindow(…);

3. 建立消息泵。  

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

打個(gè)比方,我們?cè)谝粋€(gè)自動(dòng)化的廠房里生產(chǎn)設(shè)備?;谡?guī),我們會(huì)首先定義好該設(shè)備的模板,這就是創(chuàng)建窗口類,這里”類”更多表示類別的意思。模板定義完畢,我們可以正式生產(chǎn)設(shè)備了,這就是創(chuàng)建窗口,這個(gè)CreateWindow的時(shí)候會(huì)通過字符串來匹配到我們定義的模板(窗口類)。創(chuàng)建成功后,我們要讓設(shè)備動(dòng)起來,就要像人一樣,體內(nèi)一定要有類似于血液的流傳機(jī)制,把命令傳達(dá)到設(shè)備的各個(gè)部分,這就是消息泵,這個(gè)泵就像我們的心臟一樣,源源不斷的通過GetMessage并Dispatch來分發(fā)血液(消息)。既然我們通過消息來對(duì)設(shè)備下達(dá)指令,那么就要有消息隊(duì)列來存儲(chǔ)消息,在Windows中,線程為基本的調(diào)度單位,這個(gè)消息隊(duì)列就在線程上,當(dāng)循環(huán)使用GetMessage時(shí),就是在當(dāng)前線程的消息隊(duì)列中任勞任怨的取出消息,然后分發(fā)到對(duì)應(yīng)的窗口中去。

那么具體到WPF,它又是一個(gè)怎么樣的情況,如何和老的技術(shù)兼容,又有什么新的突破呢?

WPF引入了Dispatcher的概念,這個(gè)Dispatcher的主要功能類似于Win32中的消息隊(duì)列,在它的內(nèi)部函數(shù),仍然調(diào)用了傳統(tǒng)的創(chuàng)建窗口類,創(chuàng)建窗口,建立消息泵等操作。Dispatcher本身是一個(gè)單例模式,構(gòu)造函數(shù)私有,暴露了一個(gè)靜態(tài)的CurrentDispatcher方法用于獲得當(dāng)前線程的Dispatcher。對(duì)于線程來說,它對(duì)Dispatcher是一無所知的,Dispatcher內(nèi)部維護(hù)了一個(gè)靜態(tài)的List_dispatchers, 每當(dāng)使用CurrentDispatcher方法時(shí),它會(huì)在這個(gè)_dispatchers中遍歷,如果沒有找到,則創(chuàng)建一個(gè)新的Dispatcher對(duì)象,加入到_dispatchers中去。Dispatcher內(nèi)部維護(hù)了一個(gè)Thread的屬性,創(chuàng)建Dispatcher時(shí)會(huì)把當(dāng)前線程賦值給這個(gè)Thread的屬性,下次遍歷查找的時(shí)候就使用這個(gè)字段來匹配是否在_dispatchers中已經(jīng)保存了當(dāng)前線程的Dispatcher。

那么這個(gè)創(chuàng)建窗口,建立消息泵又是什么時(shí)候被調(diào)用的呢?在Dispatcher內(nèi)部,維護(hù)了一個(gè)HwndWrapper的字段,在Dispatcher的構(gòu)造函數(shù)中,調(diào)用了HwndWrapper的構(gòu)造函數(shù),這個(gè)創(chuàng)建窗口類,創(chuàng)建窗口就是在這個(gè)函數(shù)中被調(diào)用的。這里實(shí)際的類是MessageOnlyHwndWrapper,這個(gè)Message-Only,是Windows編程中常用的伎倆,創(chuàng)建一個(gè)隱藏窗口,僅僅用來派發(fā)消息。那么循環(huán)讀取消息的消息泵又是什么時(shí)候建立起來的呢?

Dispatcher對(duì)外提供了一個(gè)靜態(tài)的Run函數(shù),顧名思義,就是啟動(dòng)Dispatcher,在函數(shù)內(nèi)部,調(diào)用了PushFrame函數(shù),在這個(gè)函數(shù)中,可以找到熟悉的GetMessage, TranslateAndDispatchMessage。那么這個(gè)PushFrame是怎么回事,F(xiàn)rame這個(gè)概念又是如何而來的呢?

這個(gè)就是WPF線程模型引入的一個(gè)新的概念,嵌套消息泵,就是在一個(gè)While(GetMessage(...))內(nèi)部又啟動(dòng)了一個(gè)While(GetMessage(...))。每調(diào)用一次PushFrame,就會(huì)啟動(dòng)一個(gè)新的嵌套的消息泵。每調(diào)用一次GetMessage,就在線程的消息隊(duì)列中取出一個(gè)消息,直至取出WM_QUIT的時(shí)候GetMessage才返回False。這個(gè)GetMessage函數(shù)Windows內(nèi)部進(jìn)行了處理,當(dāng)消息隊(duì)列為空時(shí),掛起執(zhí)行線程,避免死循環(huán)的發(fā)生。關(guān)于嵌套消息泵的優(yōu)缺點(diǎn),我們稍后再講,先來看看Dispatcher是如何處理任務(wù)的:

Windows中定義了很多Message,以WM_開頭,在注冊(cè)窗口類的時(shí)候需要設(shè)置窗口過程函數(shù),GetMessage取得的消息再分發(fā)到窗口過程函數(shù)中,整個(gè)過程為: 

WPF線程模型和Dispatcher怎么用

這個(gè)圖來自于侯捷的經(jīng)典書籍《深入淺出MFC》,1.首先創(chuàng)建Window并指定窗口的過程函數(shù)WndProc。2.當(dāng)窗口創(chuàng)建時(shí)一個(gè)WM_CREATE被放入到消息隊(duì)列中,3.消息泵通過GetMessage取得該消息后分發(fā)到窗口,窗口過程函數(shù)處理這個(gè)WM_CREATE消息…

那么WPF線程模型的Dispatcher在這個(gè)過程中扮演了什么角色呢?前面的1,2,3仍然如此,當(dāng)窗口過程函數(shù)接收到消息時(shí),它需要根據(jù)消息的類別把Windows消息轉(zhuǎn)譯成內(nèi)部的RoutedEvent或者調(diào)用布局函數(shù)等來處理。前面提到了Dispatcher主要功能類似于Win32中的消息隊(duì)列,這個(gè)隊(duì)列中存放的對(duì)象是DispatcherOperation,這個(gè)DispatcherOperation,顧名思義,就是把每一個(gè)執(zhí)行項(xiàng)封裝成一個(gè)對(duì)象,類似:

WPF線程模型和Dispatcher怎么用

這個(gè)隊(duì)列的類型為PriorityQueue,是一個(gè)含有優(yōu)先級(jí)的隊(duì)列。WPF定義了這個(gè)優(yōu)先級(jí)DispatcherPriority,有

WPF線程模型和Dispatcher怎么用

當(dāng)對(duì)這個(gè)PriorityQueue調(diào)用DeQueue時(shí),就會(huì)取出優(yōu)先級(jí)***的任務(wù)。那么這個(gè)隊(duì)列中的任務(wù)是什么時(shí)候被添加的,又是什么時(shí)候被取出執(zhí)行的呢?

Dispatcher暴露了兩個(gè)方法,Invoke和BeginInvoke,這兩個(gè)方法還有多個(gè)不同參數(shù)的重載。其中Invoke內(nèi)部還是調(diào)用了BeginInvoke,一個(gè)典型的BeginInvoke參數(shù)如下:

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

在這個(gè)BeginInvoke內(nèi)部,會(huì)把執(zhí)行函數(shù)method與參數(shù)args封裝成DispatcherOperation,并按priority加入到PriorityQueue中,這個(gè)返回值就是內(nèi)部創(chuàng)建的DispatcherOperation。也就是說每調(diào)用一次Invoke和BeginInvoke,就向Dispatcher中加入了一個(gè)任務(wù),那么這個(gè)任務(wù)什么時(shí)候被執(zhí)行呢?

DispatcherPriority定義了很多優(yōu)先級(jí),WPF將這些優(yōu)先級(jí)主要分成兩類。前臺(tái)優(yōu)先級(jí)和后臺(tái)優(yōu)先級(jí),其中前臺(tái)包括Loaded~Send,后臺(tái)包括Background~Input。剩下的幾個(gè)優(yōu)先級(jí)除了Invalid和Inactive都屬于空閑優(yōu)先級(jí),處理順序同后臺(tái)優(yōu)先級(jí)。這個(gè)前臺(tái)優(yōu)先級(jí)和后臺(tái)優(yōu)先級(jí)的分界線是以Input來區(qū)分的,這里的Input指的是鍵盤輸入和鼠標(biāo)移動(dòng)、點(diǎn)擊等等。ProrityQueue的來源有:

WPF線程模型和Dispatcher怎么用

當(dāng)然,這里Hwnd級(jí)別Hook到的消息最終也是調(diào)用Dispatcher的Invoke/BeginInvoke方法加入到Dispatcher的隊(duì)列中去的。當(dāng)處理這個(gè)PriorityQueue時(shí),會(huì)首先取得隊(duì)列中的***優(yōu)先級(jí),如果它屬于前臺(tái)優(yōu)先級(jí),執(zhí)行。如果屬于后臺(tái)優(yōu)先級(jí),那么它要去掃描線程的消息隊(duì)列,看看其中是由有類似WM_MOUSEMOVE之類的Input消息。如果沒有,執(zhí)行。如果存在,則放棄執(zhí)行,并啟動(dòng)一個(gè)Timer,當(dāng)Timer喚起時(shí)繼續(xù)判斷是否可以執(zhí)行。

那么處理PriorityQueue的時(shí)機(jī)呢?當(dāng)你調(diào)用BeginInvoke,向隊(duì)列中加入執(zhí)行項(xiàng)的同時(shí),也會(huì)調(diào)用處理Queue的判斷。判斷邏輯和上面類似,隊(duì)列中***優(yōu)先級(jí)是前臺(tái)優(yōu)先級(jí),向隱藏窗口PostMessage,這個(gè)消息是Disptcher使用RegisterWinodwMessage注冊(cè)的自定義消息。然后在GetMessage的時(shí)候如果取出這個(gè)自定義消息,則處理PriorityQueue。如果是后臺(tái)優(yōu)先級(jí),掃描線程消息隊(duì)列的Input消息,決定是否啟動(dòng)Timer還是PostMessage。

舉個(gè)例子,在后臺(tái)線程中向UI線程中使用Invoke來發(fā)送請(qǐng)求,經(jīng)歷的過程為:

1. 調(diào)用Invoke,對(duì)傳入的參數(shù)DispatcherPriority進(jìn)行判斷,如果是Send,這是個(gè)特殊的優(yōu)先級(jí),直接切換線程上下文,執(zhí)行任務(wù)并返回。如果是其他的優(yōu)先級(jí),調(diào)用BeginInvoke。

2. 在BeginInvoke中,把傳入的Delegate和參數(shù)封裝成DispatcherOperation,加入到PriorityQueue中。

3. 調(diào)用隊(duì)列處理的請(qǐng)求函數(shù),希望處理PriorityQueue。

4. 如果隊(duì)列中***優(yōu)先級(jí)屬于前臺(tái)優(yōu)先級(jí),調(diào)用PostMessage向隱藏窗口發(fā)送自定義消息。后臺(tái)處理這里省略不表。

5. 在GetMessage中取得消息并分發(fā)到隱藏窗口,這里使用的是常見的SubWindow(注釋一),消息通過Hook發(fā)送到Dispatcher的WndProcHook函數(shù)進(jìn)行處理。

6. 在WndProcHook中,如果接收到的Window消息是Dispatcher自定義的消息,則真正處理PriorityQueue。

7. 處理PriorityQueue,從中取出一個(gè)任務(wù),進(jìn)行前后臺(tái)優(yōu)先級(jí)判斷,決定是否處理還是啟動(dòng)Timer稍后處理。

回過頭來,說一說嵌套的消息循環(huán),這個(gè)要從模態(tài)對(duì)話框說起,一個(gè)通常的模態(tài)對(duì)話框場景如下:

SomeCodeA();

bool? result = dlg.ShowDialog();

SomeCodeB();

代碼運(yùn)行在UI線程中,當(dāng)執(zhí)行到dlg.ShowDialog時(shí),啟動(dòng)模態(tài)對(duì)話框,等待用戶點(diǎn)擊Yes/No或者關(guān)閉對(duì)話框,對(duì)話框關(guān)閉后程序繼續(xù)執(zhí)行SomeCodeB代碼。那么程序要在SomeCodeB處等待ShowDialog返回后才繼續(xù)執(zhí)行。當(dāng)然你可以使用WaitHandle來同步,不過這個(gè)需要掛起當(dāng)前(UI)線程,如果主窗口中有動(dòng)畫等UI動(dòng)作,那么會(huì)停止得不到響應(yīng)。這里WPF使用的是PushFrame,就是在ShowDialog內(nèi)部又建立起了一個(gè)消息泵。While(GetMessage(…))。一方面,可以確保UI線程中的消息可以被處理;另一方面,因?yàn)槭荳hile循環(huán),在對(duì)話框關(guān)閉時(shí)返回,可以確保SomeCodeB的執(zhí)行順序。

那么是不是這個(gè)嵌套的消息循環(huán)真的如此***呢?當(dāng)然不是,它打開了一扇門的同時(shí),也打開了另一扇門。一個(gè)情景,當(dāng)收到WM_SIZE消息的時(shí)候,Layout系統(tǒng)開始處理,如果在這個(gè)處理過程中,又啟動(dòng)了PushFrame,那么嵌套的消息泵就會(huì)繼續(xù)從消息隊(duì)列中取出消息,如果下一個(gè)消息也是WM_SIZE,那么進(jìn)行處理。假設(shè)這個(gè)消息處理結(jié)束后這個(gè)嵌套的消息泵返回了,那么***個(gè)WM_SIZE得以繼續(xù)處理。這樣就發(fā)生了錯(cuò)誤,本來12的處理順序變成了121。當(dāng)然這種情況不僅僅發(fā)生在Layout中,所以WPF在Dispatcher中加入了一個(gè)DisableProcessing函數(shù),在Layout等關(guān)鍵過程中調(diào)用了這個(gè)函數(shù),在這個(gè)過程中停止pump消息和禁止PushFrame。

在WPF中,所有的WPF對(duì)象都派生自DispatcherObject,DispatcherObject暴露了Dispatcher屬性用來取得創(chuàng)建對(duì)象線程對(duì)應(yīng)的Dispatcher。鑒于線程親緣性,DispatcherObject對(duì)象只能被創(chuàng)建它的線程所訪問,其他線程修改DispatcherObject需要取得對(duì)應(yīng)的Dispatcher,調(diào)用Invoke或者BeginInvoke來投入任務(wù)。一個(gè)UI線程至少有一個(gè)Dispatcher來建立消息泵處理任務(wù),一個(gè)Dispatcher只能對(duì)應(yīng)一個(gè)UI線程。那么UI線程和Render線程又如何呢?

開篇提到,WPF線程模型一分為二,一個(gè)是UI線程,一個(gè)是Render線程。這兩個(gè)被設(shè)計(jì)成分離的關(guān)系,通過Channel(event)來進(jìn)行通信。兩者之間的數(shù)量關(guān)系是一個(gè)WPF進(jìn)程只能有一個(gè)Render線程,旦可以有大于等于一個(gè)的UI線程。通常情況下是一個(gè)UI線程,也就是一個(gè)Dispatcher,那么什么情況下需要建立多個(gè)呢?

大多情況下是不需要的,少數(shù)情況下,比如MediaElement,或者Host其他ActiveX控件,我們期望在其他線程中創(chuàng)建,以提高性能。可以新建線程,在新線程中創(chuàng)建控件,并調(diào)用Dispatcher.Run啟動(dòng)Dispatcher。這樣主Window和新控件就處在不同線程中,兩者間的通信可以使用VisualTarget連接視覺樹或者使用D3DImage拷貝新控件到主Window中顯示。

開篇有益,WPF沒有什么全新的技術(shù),但提出了很多新的概念。

注釋一:  SubWindow,子窗口子類化。通常情況,所有同類別Window會(huì)共用同一個(gè)消息處理函數(shù)WndProc,子Window可以調(diào)用SetWindowLong用SubWndProc替換WndProc,這個(gè)通常稱為Sub-Window。

關(guān)于“WPF線程模型和Dispatcher怎么用”這篇文章就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,使各位可以學(xué)到更多知識(shí),如果覺得文章不錯(cuò),請(qǐng)把它分享出去讓更多的人看到。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請(qǐng)聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI