撰寫Windows Phone 7程式之前 - 了解WPF Thread Model的概念
最近在閱讀相關Windows Phone 7開發文件與Blog上的範例程式時,總是會看到「Dispatcher.BeginInvoke();」
這樣的關鍵字,該段指令是為了經由非同步的方式執行指定的委派(可能是Method或是類別)。
因為之前對於Silverlight的開發經驗可以說完全是零,因此,想說撰寫WP7程式前,基本的Silverlight概念還是要有的。
我個人認為Threading的處理是非常需要注意的,為什麼呢?
因為Threading的控制,最簡單的例子就是可以把UI的控制與背後事件分開,讓事件處理過程中,UI仍可以操作完成需
要的動作。當然還有其他很好的例子。那就繼續往下看概念的說明吧。
‧WPF執行緒模型(WPF Thread Model)
通常WPF應用程式(Silverlight也是WPF的一部分)開始時會有二個執行緒:Rendering Thread與UI Thread。
Rendering Thread主要在背景執行協助UI Thread完成工作;UI Thread故名思義就是處理Input、Event與程式碼的部分。
因此,最常使用到的即是UI Thread,常見的Button Event、WPF Object的操作都是在這個Thread完成。而每個WPF程式
至少有一個Dispatcher物件(以及一個UI Thread),所有UI上的工作項目(例如:input event或paint event)都是由UI Thread進行。
然而,大部分WPF物件的操作都是繫結到UI Thread上,其他的Thread如果直接想要操作這些物件是不被允許的。這樣的關
聯性,被稱做為「Thread Affinity」。
Thread Affinity主要是經由Dispatcher類別來進行處理,讓不同的Thread之間可以互相的操作。Dispatcher會依據用戶針對工作
項目註冊時所指定的DispatcherPriority加以佇列起來,透過a prioritized message loop的迴圈將工作項目傳送給WPF進行執行。
另外,其實大部分WPF物件(工作項目)是衍生DispatcherObejct而來,而DispatcherObject會儲存負責它的Dispatcher與所屬的執行
緒,因此,Dispatcher(或Thread)操作DispatcherObject時,將根據每個DispatcherObject是否能夠被存取與相對應的優先權來處理。
那麼DispatcherObject是什麼呢?
從上圖可以知大部分的WPF類別均集中衍生自DispatcherObject類別。該類別主要工作有二個:
(1) 提供物件所繫結現行的Dispatcher的存取權;(2) 提供方法檢查呼叫它的執行緒是否具有存取權。
每個DispatcherObject在被建立時會記錄建立它的Dispatcher來產生關聯,可透過CheckAccess與VerifyAccess(二個公開的方法)
分別來加以判斷是否能存取該DispatcherObject。
>CheckAccess:用來判斷呼叫的執行是否能夠操作這個DispatcherObject。回傳Boolean表示是否呼叫者可以操作。
>VerifyAccess:判斷目前執行緒是否可以存取這個DispatcherObject的執行緒。如果不行,即送出InvalidOperationException。
另外,只有建立Dispatcher的Thread(以UI Thread為主)才可以直接存取DispatcherObject(也就代表是不能讓其他Dispatcher來直接
操作的)。如果外部的Thread想要存取,則需要在與該DispatcherObject相關聯的Dispatcher上呼叫BeginInvoke或Invoke才能存取。
1: public class CustWPFObject: DispatcherObject
2: {
3: public void HanldeWork()
4: {
5: VerifyAccess();
6: //確認目前運作的執行續能處理該物件後,則可以進行操作的流程。
7: }
8:
9: public void CheckCanWork()
10: {
11: if (CheckAccess())
12: {
13: //如果是true,代表呼叫的執行能夠往下處理該物件的流程 。
14: }
15: }
16:
17: }
了解整個Thread運作的單位(DispatcherObject)之後,接下來便是進一步去了解Dispatcher所扮演的角色與運作的方式。
‧Dispatcher
UI Thread會把工作項目佇列於Dispatcher物件之中,Dispatcher會依照DispatcherPriority依序完成工作項目。Dispatcher又被稱為
WPF中的Message pump,提供一個機制去route工作項目給UI Thread進行處理。當Dispatcher將工作項目轉給UI Thread時,會讓
UI Thread進入封鎖狀態(畫面鎖定,因為要處理工作項目),因此,通常「Dispatcher每個佇列的工作項目應該是小且快速回應的」。
這樣可以保持回應是快速不會造成用戶需要等待UI Thread被封鎖過長的問題。所以處理時間會太長的工作應該被分解到不同的
Thread中去處理(例如:ThreadStart delegate),然後需要UI Thread處理的部分應該是少量多餐的分割方式。
那麼該怎麼撰寫使用Dispatcher的程式呢?舉個例來說:如果我今天有一個運算是需要透過WCF取回資料時,為了不讓用戶去
等待呼叫時造成的畫面鎖定問題,程式碼可以如下之撰寫方法:(程式碼範例可參考<使用 Dispatcher 建置回應性更佳的應用程式>)
A. 使用Dispatcher提供的Invoke方法,直接在UI Thread上呼叫程式碼。
1: //工作的處理被在不同於UI Thread上被完成
2: ThreadStart start = delegate()
3: {
4: MyWFCService tSerivce = New MyWCFService();
5: string tMsg = TService.GetData("1000");
6:
7: //使用Invoke(同步)將取得的結果顯示於setReusult物件上。
8: Dispatcher.Invoke(DispatcherPriority.Normal,
9: New Action<string>(setResult), tMsg);
10: }
11: new Thread(start).Start();
>Dispatcher.Inovoke提供豐富的多型可以使用,透過指定的DispatcherPriority、delegate action與指定的參數值。
呼叫Invoke方法之後,它會將指定在UI Thread上執行的delegate action排入Dispatcher佇列中。使用該方法可確保UI Thread會進行封鎖,
直到在 UI Thread上執行該工作為止。
B. 使用Dispatcher提供的BeginInvoke方法,在UI Thread上呼叫程式碼。
1: //工作的處理被在不同於UI Thread上被完成
2: ThreadStart start = delegate()
3: {
4: MyWCFService tService = new MyWCFService();
5: string tMsg = tService.GetData("100");
6:
7: //使用BeginInvoke(非同步)將取得的結果顯示於setResult物件上。
8: DispatcherOperation tDOP = Dispatcher.BeginInvoke(
9: DispatcherPriority.Normal,
10: new Action<string>(setResult), tMsg);
11:
12: DispatcherOperationStatus tStatus = tDOP.Status;
13: while (tStatus != DispatcherOperationStatus.Completed)
14: {
15: //非同步檢查目前的執行狀態
16: tStatus = tDOP.Wait(TimeSpan.FromMilliseconds(1000));
17: if (tStatus == DispatcherOperationStatus.Aborted)
18: {
19: //顯示失敗的訊息
20: }
21: }
22: };
23:
24: // Create the thread and kick it started!
25: new Thread(start).Start();
使用BeginInvoke方法,屬於非同步的方法,因此,當呼叫指令完成之後控制權將會歸還給呼叫的物件。
BeginInovke方法的執行結果會回傳一個DispatcherOperation物件。該DispatcherOperation物件具有與指定的委派進行互動,例如:
(a) 在委派於事件佇列中暫止執行時,變更委派的DispatcherPriority(使用DispatcherOperation的Prority屬性);
(b) 從事件佇列中移除委派(使用DispatcherOperation中的Abort());
(c) 等待委派傳回(使用DispatcherOperation中的Wait()或Wait(TimeSpan);
(d) 取得執行委派之後回傳的值(使用DispatcherOperation的Result屬性)。
DispatcherPriority的列舉清單,其中以Send為最高優先權,通常使用的權限大多是屬於Normal與SystemIdle二種,
當然,如果所設計的WPF程式需要很多背景來完成的任務,也可以使用Background與ContextIdle二種列舉值。
設定的列舉值均會影響Dispatcher在prioritized message loop中選取工作項目的處理。特別注意三個當WPF程式閒置
時可以使用的列舉值:SystemIdle、ApplicationIdle與ContextIdle有助於在工作量少的情況下完成較低負荷的工作項目。
‧BackgroundWorker
BackgroundWorker元件的介紹,可以參考一系列的<多執行緒初探--使用BackgroundWorker(1)>。其重點說明在於,
在開發WPF程式時如有遇到需要長時間運算或與資料庫進行資料操作的作業時,因為長時間的處理會造成UI變成沒
辦法繼續回應用戶的操作,為了讓耗時的作業可以完成又希望能繼續回應用戶的操作,BackgroundWorker元件就是個
不錯的選擇方式。然而幾個使用時的重點如下:
1: private BackgroundWorker gBW = new BackgroundWorker();
2:
3: private void Init()
4: {
5: //設定該BackgroundWorker是否可以報告進度
6: gBW.WorkerReportProgress = true;
7:
8: //設定該BackgroundWorker是否支援非同步取消
9: gBW.WorkerSupportsCancellation = true;
10:
11: //如果設定該BackgroundWoker於背景作業,則需為DoWork加入事件處理常式。
12: //使用DoWorkEventHandler類別
13: gBW.DoWork += new DoWorkEventHandler(BW_DoWork);
14:
15: //如設定了WorkerReportProgress代表要回報處理進度,因此要為ProgressChanged設定事件處理常式。
16: //使用ProgressChangedEventHandler類別
17: gBW.ProgressChanged += new ProgressChangedEventHandler(BW_ProgressChanged);
18:
19: //若要在作業完成時接收告知,請處理 RunWorkerCompleted 事件
20: //使用RunWorkerCompletedEventHandler類別
21: gBW.RunWorkerCompleted += new RunWorkerCompletedEventHandler(BW_RunWorkerCompleted);
22:
23: //執行BackgroundWorker
24: gBW.RunWorkerAsync();
25: }
BackgroundWorker元件適用於WPF,因為它實際上使用AsyncOperationManager類別,該類別使用SynchronizationContext
類別來處理同步化的工作。然而該SynchronizationContext類別在不同的應用上有不同的衍生類別,例如:
在Windows Forms上,有衍生的:WindowsFormsSynchronizationContext;
在ASP.NET上,有衍生的:AspNetSynchronizationContext。
‧DispatcherTimer
「整合至 Dispatcher 佇列中的計時器,會在指定的時間間隔以指定的優先權處理這個佇列。」它與Timer不同在於,
它支援Queue,透過Loop方式定期完成Queue的工作。如果是定期擷取遠端資料庫或運算的情形,不妨使用該類別,
可以省去另外撰寫指定委派UI Thread的dispatcher。可以參考<Tip: Asynchronous Silverlight - Execute on the UI thread>
另外,由於該類別會繫結到Dispatcher,所以支援指定的DispatcherPriority和要使用的Dispatcher來定期執行指定的Dispatcher。
DispatcherTimer預設的Priority是Normal。
1: //建立一個DispatcherTimer元件
2: DispatcherTimer tTimer = new DispatcherTimer(DispatcherPriority.SystemIdel, form1.Dispatcher);
3:
4: //指定該Timer間隔時間
5: tTimer.Interval = TimeSpan.FromMilliseconds(1000);
6:
7: //指定間隔時間事件要處理的處理常式
8: tTimer.Tick += new EventHandler(deletegate(object sender, EventArgs e )
9: {
10: txtNoew.Text += (gSecound + 1).toString();
11: });
12:
13: //啟動Timer
14: tTimer.Start();
以上是整理References中針對WPF Thread Model描述的內容,主要在於釐清一些單執行緒與多執行緒運作的觀念,
並且協助了解如何設計背景的作業才能讓產生的應用程式可以快速回應不會有被封鎖過長的現象。我個人認為這
也是將會影響在設計WP 7程式時需要特別注意的地方。分享給大家並做為個人的筆記。
[補充]
‧DispatcherSynchronizationContext
References:
‧Property change notifications for multithreaded Silverlight applications
‧http://msdn.microsoft.com/zh-tw/library/system.windows.threading.dispatcher(v=VS.95).aspx
‧Dispatcher in Silverlight – Extension Method
‧Tip: Asynchronous Silverlight - Execute on the UI thread
‧Build More Responsive Apps With The Dispatcher
‧Silverlight Threading - Getting back to the UI thread
‧http://www.drdobbs.com/windows/207602773