參考:http://www.cnblogs.com/zhouyinhui/archive/2008/01/27/1055261.html
第一次做WPF項(xiàng)目,要用到多線程,委托,自定義事件等技術(shù)??戳藥灼W(wǎng)友技術(shù)文章覺得不錯(cuò),做下筆記。
在多線程編程中,我們經(jīng)常會(huì)需要在子線程中訪問主線程的內(nèi)容,特別是更改主窗體的UI界面內(nèi)容,如果直接訪問跨線程資源,系統(tǒng)就會(huì)報(bào)線程訪問錯(cuò)誤.查了MSDN發(fā)現(xiàn)由于這樣的做法是線程不安全的。在WinForm中窗體類提供了Invoke方法以從子線程中訪問主線程資源.在WPF中Window類并沒有Invoke方法,但是WPF提供了專門負(fù)責(zé)線程調(diào)度工作的Window.Dispatcher類,每個(gè)線程都有一個(gè),我們在一個(gè)線程中要讓另一個(gè)線程做事情,其實(shí)就是調(diào)用目標(biāo)Dispatcher調(diào)度完成.
Dispatcher
WPF規(guī)定了(事實(shí)上在.net2.0中便已規(guī)定了)UI元素只能由創(chuàng)建該元素的線程來訪問。比如我們從新開的一個(gè)線程中訪問主界面中的元素會(huì)出現(xiàn)運(yùn)行時(shí)的異常。Dispatcher來維持著這一規(guī)定,并組織著消息循環(huán)。Dispatcher負(fù)責(zé)檢測訪問對象的線程與對象創(chuàng)建線程是否一致,不一致則拋出異常。值得一提的是,Dispatcher的消息循環(huán)中的Work Item是有優(yōu)先級的,這可以讓高優(yōu)先級的項(xiàng)能有更多的工作時(shí)間。比如界面繪制比處理用戶輸入的優(yōu)先級要高,這使得界面動(dòng)畫更加流暢。這也就是為什么,我們在調(diào)用Dispatcher.Invoke ( DispatcherPriority,…) 與Dispatcher. BeginInvoke (DispatcherPriority,…)要傳入一個(gè)優(yōu)先級參數(shù)的原因。下面是對各個(gè)優(yōu)先級的說明:
優(yōu)先級 | 說明 |
Inactive | 工作項(xiàng)目已排隊(duì)但未處理。 |
SystemIdle | 僅當(dāng)系統(tǒng)空閑時(shí)才將工作項(xiàng)目調(diào)度到 UI 線程。這是實(shí)際得到處理的項(xiàng)目的最低優(yōu)先級。 |
ApplicationIdle | 僅當(dāng)應(yīng)用程序本身空閑時(shí)才將工作項(xiàng)目調(diào)度到 UI 線程。 |
ContextIdle | 僅在優(yōu)先級更高的工作項(xiàng)目得到處理后才將工作項(xiàng)目調(diào)度到 UI 線程。 |
Background | 在所有布局、呈現(xiàn)和輸入項(xiàng)目都得到處理后才將工作項(xiàng)目調(diào)度到 UI 線程。 |
Input | 以與用戶輸入相同的優(yōu)先級將工作項(xiàng)目調(diào)度到 UI 線程。 |
Loaded | 在所有布局和呈現(xiàn)都完成后才將工作項(xiàng)目調(diào)度到 UI 線程。 |
Render | 以與呈現(xiàn)引擎相同的優(yōu)先級將工作項(xiàng)目調(diào)度到 UI 線程。 |
DataBind | 以與數(shù)據(jù)綁定相同的優(yōu)先級將工作項(xiàng)目調(diào)度到 UI 線程。 |
Normal | 以正常優(yōu)先級將工作項(xiàng)目調(diào)度到 UI 線程。這是調(diào)度大多數(shù)應(yīng)用程序工作項(xiàng)目時(shí)的優(yōu)先級。 |
Send | 以最高優(yōu)先級將工作項(xiàng)目調(diào)度到 UI 線程。 |
上面提到了Dispatcher維持著一個(gè)規(guī)矩“只有創(chuàng)建該對象的線程可以訪問該對象”。這里的對象不僅僅是指一些UI控件(比如Button),而是所以的派生于DispatcherObject類的對象。
對于阻塞的操作,不一定需要開啟新線程
當(dāng)我們遇到某個(gè)費(fèi)時(shí)的操作是,第一反映往往是開啟一個(gè)新線程,然后在后臺(tái)去處理它,以便不阻塞我們的用戶界面。當(dāng)然,這是正確的想法。當(dāng)并不是所有的都需如此。仔細(xì)想想界面阻塞的原因,我們知道其是由于時(shí)間被消耗在某個(gè)費(fèi)時(shí)的Work Item上了,而那些處理用戶界面的Work Item還在那里苦苦等候。So,我們只要?jiǎng)e讓他們苦苦等候就可以了,只要用戶有界面操作我們就處理,線程上的其他空閑時(shí)間來處理我們的復(fù)雜操作。我們將復(fù)雜的費(fèi)時(shí)的操作細(xì)化成很多不費(fèi)時(shí)的小操作,在這些小操作之間的空隙處我們來處理相應(yīng)用戶界面的操作
阻塞的情況如下, MouseUp與MouseLeave會(huì)被阻塞:
(MouseDown)->(費(fèi)時(shí)的,復(fù)雜操作)->(MouseUp)->(MouseLeave)…
細(xì)化后的情況如下,MouseUp與MouseLeave不會(huì)被阻塞:
(MouseDown)->(不費(fèi)時(shí)的,小操作,復(fù)雜操作的1/n)->(MouseUp)->(不費(fèi)時(shí)的,小操作,復(fù)雜操作的1/n) -> (MouseLeave)…
舉一個(gè)簡單的例子,假定我們的主界面上要顯示一個(gè)數(shù)字,其為Window1的CurrentNum屬性,我們已經(jīng)將界面上的某個(gè)TextBlock與其綁定了起來:
當(dāng)我們點(diǎn)擊界面上的一個(gè)按鈕后,要求該數(shù)字被不停的累加,直到再次點(diǎn)擊該按鈕是停止.實(shí)際效果相當(dāng)于:
如果我們直接按照上面的While語句來書寫程序,明顯,當(dāng)用戶點(diǎn)擊按鈕后,整個(gè)線程將在這里被堵死,界面得不到更新,用戶也沒有辦法再次點(diǎn)擊按鈕來停止這個(gè)循環(huán),遭透了。
既不開啟新線程又不阻塞界面應(yīng)該怎么辦呢?
我們知道this.CurrentNum++;語句以及更新綁定到CurrentNum屬性的TextBlock并不耗費(fèi)時(shí)間的,耗費(fèi)時(shí)間的是他們的累加而成的死循環(huán),所以,我們將這個(gè)循環(huán)分解成無數(shù)個(gè)僅僅由this.Current++語句組成的小方法,并在這些小方法的之間來處理用戶界面:
上面的兩段代碼可以簡單地如下示意:
阻塞的情況如下, MouseUp與MouseLeave會(huì)被阻塞:
(MouseDown)->(費(fèi)時(shí)的While(true))->(MouseUp)->(MouseLeave)…
細(xì)化后的情況如下,MouseUp與MouseLeave不會(huì)被阻塞:
(MouseDown)->(不費(fèi)時(shí)的CalNum)->(MouseUp)->(不費(fèi)時(shí)的CalNum) -> (MouseLeave)…
用Delegate.Invoke()或Delegate.BeginInvoke()來開啟新線程
除了new 一個(gè)Thread對象外,使用Delegate的Invoke或BeginInvoke方法也可以開啟新的線程。
假設(shè)有下面這一個(gè)很費(fèi)時(shí)的方法,我們應(yīng)該如何使用Delegate來改造呢
首先,我們聲明一個(gè)可以用于TheHugeMethod方法的代理:
然后對TheHugeMethod構(gòu)造一個(gè)NormalMethod類型的對象,并調(diào)用其Invoke方法(同步調(diào)用)或BeginInvoke方法(異步調(diào)用)
由于是開啟了新的線程,所以TheHugeMethod方法中對this.button_Test控件的調(diào)用語句也得改造一下:
在新線程中執(zhí)行消息循環(huán)
一般情況下我們不需要在新線程中執(zhí)行消息循環(huán)了,因?yàn)槲覀兂3J窃谛戮€程中執(zhí)行一些后臺(tái)操作而不需要用戶在新線程中執(zhí)行UI操作(比如我們在新線程中從網(wǎng)絡(luò)上下載一些數(shù)據(jù)然后UI線程來顯示這些數(shù)據(jù))。當(dāng)有時(shí)新線程卻是需要消息循環(huán)的,最簡單的例子是操作系統(tǒng)的“資源管理器”,每一個(gè)資源管理器窗口都在一個(gè)單獨(dú)的線程中(它們都在同一個(gè)進(jìn)程中)。
但當(dāng)你按照如下方式編寫代碼來新建一個(gè)資源管理器窗口時(shí),會(huì)出問題:
問題是newWindow閃現(xiàn)一下就消失了。因?yàn)樵撔麓翱跊]有進(jìn)入消息循環(huán),當(dāng)newWindow.Show()方法執(zhí)行完畢后,新線程的一切都結(jié)束了。
正確的方法是在newWindow.Show();方法后加入Dispatcher.Run()語句,其會(huì)將主執(zhí)行幀推入該Dispatcher的消息循環(huán)中。
BackgroundWorker實(shí)質(zhì)是:基于事件的異步模式
在多線程編程中,最爽的莫過于.net 提供了BackgroundWorker類了。其可以:
“在后臺(tái)”執(zhí)行耗時(shí)任務(wù)(例如下載和數(shù)據(jù)庫操作),但不會(huì)中斷您的應(yīng)用程序。
同時(shí)執(zhí)行多個(gè)操作,每個(gè)操作完成時(shí)都會(huì)接到通知。
等待資源變得可用,但不會(huì)停止(“掛起”)您的應(yīng)用程序。
使用熟悉的事件和委托模型與掛起的異步操作通信。
我想大家對BackgroundWorker亦是再熟悉不過了,這里就不多做介紹了,另外“基于事件的異步模式”是WinForm的內(nèi)容,但在WPF中完美運(yùn)用(原因是WPF用DispatcherSynchronizationContext擴(kuò)展了SynchronizationContext),可以參見MSDN“Event-based Asynchronous Pattern Overview”