原文: WPF 線程:使用排程程式建構反應速度更快的應用程式 作者: Shawn Wildermuth 原文: http://msdn.microsoft.com/msdnmag/issues/07/10/WPFThreading/default.aspx?loc=en-us 本文讨論:
| 本文使用了以下技術: .NET Framework 3.0, WIndows Presentation Foundation |
如
果您在建立一個直覺、自然甚至精美的界面上花費了數月時間,但結果是使用者不得不在他們的組合辦公桌上敲打着手指等待程式響應,這會讓人覺得丢臉。由于長時間運作的程序導緻應用程式的螢幕停滞不動,看到這樣的情況是一件痛苦的事情。然而,建立響應迅速的應用程式需要進行認真的規劃,這通常需要使長時間運作的程序在其他線程中工作,以便釋放出 UI 線程,使其随時跟上使用者的進度。
我第一次真正體驗響應速度可追溯到 Visual C++® 與 MFC 以及我曾經編寫的第一個網格。當時,我正在幫助編寫一個藥學應用程式,該程式必須能夠将每種藥物顯示在複雜的處方中。問題是有 30,000 種藥物,是以我們決定先在 UI 線程中填充第一個滿屏藥物(時間大約為 50 毫秒),給人一種反應迅速的印象,然後使用背景線程完成填充不可見的藥物(時間大約為 10 秒)。項目運作良好,而且我學到了非常寶貴的經驗,那就是使用者感覺可以比現實更重要。
在建立具有吸引力的使用者界面方面,Windows® Presentation Foundation (WPF) 是一項出色的技術,但這并不意味着您就不需要考慮應用程式的響應性。不管相關的長時間運作程序的類型為何(不管是從資料庫擷取大量結果,進行異步 Web 服務調用,還是任何數量的其他潛在密集型操作),簡單的事實就是,響應更快的應用程式是讓使用者更滿意的長期保證。但是,開始在 WPF 應用程式中使用異步程式設計模型之前,了解 WPF 線程模型非常重要。在本文中,我不但将會向您介紹此線程模型,還會向您展示基于排程程式的對象的工作原理,以及解釋如何使用 BackgroundWorker 以便建立具有吸引力和響應性的使用者界面。
所有 WPF 應用程式啟動時都會加載兩個重要的線程:一個用于呈現使用者界面,另一個用于管理使用者界面。呈現線程是一個在背景運作的隐藏線程,是以您通常面對的唯一線程就是 UI 線程。WPF 要求将其大多數對象與 UI 線程進行關聯。這稱之為線程關聯,意味着要使用一個 WPF 對象,隻能在建立它的線程上使用。在其他線程上使用它會導緻引發運作時異常。注意,WPF 線程模型可與基于 Win32® 的 API 進行順暢的互動。這意味着 WPF 可以承載或承載于任何基于 HWND 的 API(Windows Forms、Visual Basic®、MFC,甚至是 Win32)。
線程關聯由 Dispatcher 類處理,該類即是用于 WPF 應用程式的、按優先級排列的消息循環。通常,WPF 項目有單個 Dispatcher 對象(是以有單個 UI 線程),所有使用者界面工作均以其為通道。
與典型的消息循環不同,發送到 WPF 的每個工作項目都以特定的優先級通過 Dispatcher 進行發送。這就能夠按優先級對項目排序,并延遲某種類型的工作,直到系統有時間來處理它們。(例如,有些工作項目可被延遲到系統或應用程式處于空閑狀态時。) 支援項目優先順序使 WPF 能夠讓某種類型的工作擁有更多的權限,是以線上程上擁有比其他工作更多的時間。
在本文的後面,我将會闡明,呈現引擎在更新使用者界面方面比輸入系統具備更高的優先級。這意味着不管使用者是否正在使用滑鼠、鍵盤或墨水列印系統,動畫都将會繼續更新使用者界面。這可以使使用者界面看起來響應更快。例如,讓我們假定您正在編寫一個音樂播放應用程式(類似于 Windows Media® Player)。不管使用者是否正在使用界面,您最有可能希望顯示有關音樂播放的資訊(包括進度條和其他資訊)。對使用者來說,這可以使界面看起來對他們最感興趣的事情(在此例中為聽音樂)響應更快。
除了使用 Dispatcher 的消息循環将工作項目引導至使用者界面線程之外,每個 WPF 對象也可感覺對其負責的 Dispatcher(以及它由此所依賴的 UI 線程)。這意味着任何從第二個線程更新 WPF 對象的嘗試均會失敗。這就是 DispatcherObject 類的職責。
http://msdn.microsoft.com/msdnmag/issues/07/10/WPFThreading/default.aspx?loc=zh#contents在 WPF 的類層次結構中,大部分都集中派生于 DispatcherObject 類(通過其他類)。如圖 1 所示,您可以看到 DispatcherObject 虛拟類正好位于 Object 下方和大多數 WPF 類的層次結構之間。
圖 1 DispatcherObject 派生
DispatcherObject 類有兩個主要職責:提供對對象所關聯的目前 Dispatcher 的通路權限,以及提供方法以檢查 (CheckAccess) 和驗證 (VerifyAccess) 某個線程是否有權通路對象(派生于 DispatcherObject)。CheckAccess 與 VerifyAccess 的差別在于 CheckAccess 傳回一個布爾值,表示目前線程是否可以使用對象,而 VerifyAccess 則線上程無權通路對象的情況下引發異常。通過提供這些基本的功能,所有 WPF 對象都支援對是否可在特定線程(特别是 UI 線程)上使用它們加以确定。如果您正在編寫您自己的 WPF 對象(諸如控件),那麼您使用的所有方法都應在執行任何工作之前調用 VerifyAccess。這可確定您的對象僅在 UI 線程上使用,如
圖 2所示。
為此,在調用 Control、Window、Panel 之類的任何 DispatcherObject 派生對象時,應注意要處在 UI 線程上。如果您從非 UI 線程調用 DispatcherObject,就會引發異常。相反,如果您正在某個非 UI 線程上工作,就需要使用 Dispatcher 來更新 DispatcherObjects。
http://msdn.microsoft.com/msdnmag/issues/07/10/WPFThreading/default.aspx?loc=zh#contentsDispatcher 類提供了到 WPF 中消息泵的通道,還提供了一種機制來路由供 UI 線程處理的工作。這對滿足線程關聯要求是必要的,但是對通過 Dispatcher 路由的每個工作來說,UI 線程都被阻止,是以使 Dispatcher 完成的工作小而快非常重要。最好将使用者界面的大塊工作拆分為較小的離散塊,以便 Dispatcher 執行。任何不需要在 UI 線程上完成的工作應移到其他線程上,以便在背景進行處理。
通常,您将會使用 Dispatcher 類将工作項目發送到 UI 線程進行處理。例如,如果您想要使用 Thread 類在單獨的線程上進行一些工作,那麼可以建立一個 ThreadStart 委托,在新的線程上進行一些工作,如
圖 3此代碼執行失敗,原因是目前沒有在 UI 線程上調用對 statusText 控件(一種 TextBlock)的 Text 屬性的設定。當該代碼嘗試設定 TextBlock 上的 Text 時,TextBlock 類會在内部調用其 VerifyAccess 方法以確定該調用來自 UI 線程。當它确定調用是來自不同的線程時,則會引發異常。那麼您如何使用 Dispatcher 在 UI 線程上進行調用呢?
Dispatcher 類提供了在 UI 線程上直接調用代碼的權限。
圖 4展示了使用 Dispatcher 的 Invoke 方法來調用名叫 SetStatus 的方法,進而更改 TextBlock 的 Text 屬性。
該 Invoke 調用包含三條資訊:要執行的項目的優先級、說明要執行何種工作的委托,以及任何傳遞給第二個參數中所述委托的參數。通過調用 Invoke,它将要在 UI 線程上調用的委托排入隊列。使用 Invoke 方法可確定在 UI 線程上執行工作之前保持阻止。
作為一種異步使用 Dispatcher 的替代方法,您可以使用 Dispatcher 的 BeginInvoke 方法為 UI 線程異步排隊工作項目。調用 BeginInvoke 方法會傳回一個 DispatcherOperation 類的執行個體,其中包含有關執行工作項目的資訊,包括工作項目的目前狀态和執行的結果(如果工作項目已完成)。BeginInvoke 方法和 DispatcherOperation 類的使用如
圖 5與典型的消息泵實作不同,Dispatcher 是基于優先級的工作項目隊列。這就能夠實作更好的響應性,因為重要性更高的工作能夠在重要性較低的工作之前執行。優先順序的本質可通過 DispatchPriority 枚舉中指定的優先級加以例證(如
圖 6所示)。
一般來說,對于更新 UI 外觀的工作項目(如我之前使用的示例),您應始終使用 DispatcherPriority.Normal 優先級。但也有時候應該使用不同的優先級。其中尤其令人感興趣的是三個空閑優先級(ContextIdle、ApplicationIdle 和 SystemIdle)。通過這些優先級可以指定僅在工作負載很低的情況下執行的工作項目。
http://msdn.microsoft.com/msdnmag/issues/07/10/WPFThreading/default.aspx?loc=zh#contents現在您對 Dispatcher 的工作原理已有所了解,那麼如果得知在大多數情況下都不會使用它,您可能會感到驚訝。在 Windows Forms 2.0 中,Microsoft 引入了一個用于非 UI 線程處理的類來為使用者界面開發人員簡化開發模型。此類稱為 BackgroundWorker。
圖 7顯示了 BackgroundWorker 類的典型用法。
BackgroundWorker 元件與 WPF 的配合非常好,因為在背景它使用了 AsyncOperationManager 類,該類随之又使用 SynchronizationContext 類來處理同步。在 Windows Forms 中,AsyncOperationManager 遞交從 SynchronizationContext 類派生的 WindowsFormsSynchronizationContext 類。同樣,在 ASP.NET 中,它與 SynchronizationContext 的不同派生(稱為 AspNetSynchronizationContext)配合使用。這些 SynchronizationContext 派生的類知道如何處理方法調用的跨線程同步。
在 WPF 中,可用 DispatcherSynchronizationContext 類來擴充此模型。通過使用 BackgroundWorker,可自動應用 Dispatcher 來調用跨線程方法調用。好消息是,由于您可能已經熟悉了這個常見的模式,是以可以繼續在新的 WPF 項目中使用 BackgroundWorker。
http://msdn.microsoft.com/msdnmag/issues/07/10/WPFThreading/default.aspx?loc=zh#contents WPF 線程資源- Windows Presentation Foundation 虛拟實驗室
- .NET Framework 開發中心的 WPF
- WPF 基礎:線程模型,MSDN 庫
- “Programming the Windows Presentation Foundation”(Windows Presentation Foundation 程式設計),作者 Chris Sells 和 Ian Griffiths (O'Reilly, 2005)
在 Microsoft® .NET Framework 中定期執行代碼是開發中的一項常見任務,但是在 .NET 中使用計時器仍令人困惑。如果您在 .NET Framework 基類庫 (BCL) 中查找 Timer 類,那麼至少會找到 3 種 Timer 類:System.Threading.Timer、System.Timers.Timer 和 System.Windows.Forms.Timer。每種計時器均有所不同。Alex Calvo 在《MSDN 雜志》中的文章解釋了何時使用這些 Timer 類中的每個類(請參見
msdn.microsoft.com/msdnmag/issues/04/02/TimersinNET)。
對于 WPF 應用程式來說,有一種使用 Dispatcher(即 DispatcherTimer 類)的新型計時器。與其他計時器類似,DispatcherTimer 類支援指定滴答之間的間隔,以及在計時器事件觸發時要運作的代碼。在
圖 8中可以看到一種相當常見的 DispatcherTimer 使用方法。
因為 DispatcherTimer 類與 Dispatcher 相關聯,是以還可以指定 DispatcherPriority 以及要使用的 Dispatcher。DispatcherTimer 類使用“正常”優先級作為目前 Dispatcher 的預設優先級,但是您可以覆寫這些值:
_timer = new DispatcherTimer(
DispatcherPriority.SystemIdle, form1.Dispatcher);
規劃工作程序以獲得響應更快的應用程式,其中的一切努力都是非常值得的。開展一些初期研究工作可以使規劃更成功。我建議您在開始之前浏覽一下“WPF 線程參考”側欄中提到的一些網站以及本文章,它們會為您開發響應更快的應用程式打下良好的基礎。