原文引自:http://www.microsoft.com/china/msdn/library/architecture/architecture/architecturetopic/SCArchDeGuide/Chapter1Introduction.mspx
第 6 章 — 使用多線程
智能客戶端體系結(jié)構(gòu)與設(shè)計(jì)指南
David Hill、Brenton Webster、Edward A. Jezierski、Srinath Vasireddy 和 Mohammad Al-Sabt,Microsoft Corporation;Blaine Wastell,Ascentium Corporation;Jonathan Rasmusson 和 Paul Gale,ThoughtWorks;以及 Paul Slater,Wadeware LLC
相關(guān)鏈接
Microsoft? patterns & practices 庫: http://www.microsoft.com/resources/practices/default.mspx
.NET 的應(yīng)用程序體系結(jié)構(gòu):設(shè)計(jì)應(yīng)用程序和服務(wù)http://msdn.microsoft.com/library/en-us/dnbda/html/distapp.asp
摘要:本章討論與智能客戶端應(yīng)用程序中多線程的使用有關(guān)的問題。為了最大限度地提高智能客戶端應(yīng)用程序的響應(yīng)能力,需要仔細(xì)考慮如何和何時(shí)使用多線程。線程可以大大提高應(yīng)用程序的可用性和性能,但是當(dāng)您確定它們將如何與用戶界面交互時(shí),需要對其進(jìn)行非常仔細(xì)的考慮。

本頁內(nèi)容
![]() |
.NET Framework 中的多線程處理 |
![]() |
何時(shí)使用多線程 |
![]() |
創(chuàng)建和使用線程 |
![]() |
使用任務(wù)處理 UI 線程和其他線程之間的交互 |
![]() |
小結(jié) |
線程是基本執(zhí)行單元。單線程執(zhí)行一系列應(yīng)用程序指令,并且在應(yīng)用程序中從頭到尾都經(jīng)由單一的邏輯路徑。所有的應(yīng)用程序都至少有一個(gè)線程,但是您可以將它們設(shè)計(jì)成使用多線程,并且每個(gè)線程執(zhí)行一個(gè)單獨(dú)的邏輯。在應(yīng)用程序中使用多線程,可以將冗長的或非常耗時(shí)的任務(wù)放在后臺(tái)處理。即使在只有單處理器的計(jì)算機(jī)上,使用多線程也可以非常顯著地提高應(yīng)用程序的響應(yīng)能力和可用性。
使用多線程來開發(fā)應(yīng)用程序可能非常復(fù)雜,特別是當(dāng)您沒有仔細(xì)考慮鎖定和同步問題時(shí)。當(dāng)開發(fā)智能客戶端應(yīng)用程序時(shí),需要仔細(xì)地評估應(yīng)該在何處使用多線程和如何使用多線程,這樣就可以獲得最大的好處,而無需創(chuàng)建不必要的復(fù)雜并難于調(diào)試的應(yīng)用程序。
本章研究對于開發(fā)多線程智能客戶端應(yīng)用程序最重要的一些概念。它介紹了一些值得推薦的在智能客戶端應(yīng)用程序中使用多線程的方法,并且描述了如何實(shí)現(xiàn)這些功能。
.NET Framework 中的多線程處理
所有的 .NET Framework 應(yīng)用程序都是使用單線程創(chuàng)建的,單線程用于執(zhí)行該應(yīng)用程序。在智能客戶端應(yīng)用程序中,這樣的線程創(chuàng)建并管理用戶界面 (UI),因而稱為 UI 線程。
可以將 UI 線程用于所有的處理,其中包括 Web 服務(wù)調(diào)用、遠(yuǎn)程對象調(diào)用和數(shù)據(jù)庫調(diào)用。然而,以這種方式使用 UI 線程通常并不是 一個(gè)好主意。在大多數(shù)情況下,您不能預(yù)測調(diào)用 Web 服務(wù)、遠(yuǎn)程對象或數(shù)據(jù)庫會(huì)持續(xù)多久,而且在 UI 線程等待響應(yīng)時(shí),您可能會(huì)導(dǎo)致 UI 凍結(jié)。
通過創(chuàng)建附加線程,應(yīng)用程序可以在不使用 UI 線程的情況下執(zhí)行額外的處理。當(dāng)應(yīng)用程序調(diào)用 Web 服務(wù)時(shí),可以使用多線程來防止 UI 凍結(jié)或并行執(zhí)行某些本地任務(wù),以整體提高應(yīng)用程序的效率。在大多數(shù)情況下,您應(yīng)該堅(jiān)持在單獨(dú)的線程上執(zhí)行任何與 UI 無關(guān)的任務(wù)。
同步和異步調(diào)用之間的選擇
應(yīng)用程序既可以進(jìn)行同步調(diào)用,也可以進(jìn)行異步調(diào)用。同步 調(diào)用在繼續(xù)之前等待響應(yīng)或返回值。如果不允許調(diào)用繼續(xù),就說調(diào)用被阻塞 了。
異步 或非阻塞 調(diào)用不等待響應(yīng)。異步調(diào)用是通過使用單獨(dú)的線程執(zhí)行的。原始線程啟動(dòng)異步調(diào)用,異步調(diào)用使用另一個(gè)線程執(zhí)行請求,而與此同時(shí)原始的線程繼續(xù)處理。
對于智能客戶端應(yīng)用程序,將 UI 線程中的同步調(diào)用減到最少非常重要。在設(shè)計(jì)智能客戶端應(yīng)用程序時(shí),應(yīng)該考慮應(yīng)用程序?qū)⑦M(jìn)行的每個(gè)調(diào)用,并確定同步調(diào)用是否會(huì)對應(yīng)用程序的響應(yīng)和性能產(chǎn)生負(fù)面影響。
僅在下列情況下,使用 UI 線程中的同步調(diào)用:
? |
執(zhí)行操縱 UI 的操作。 |
? |
執(zhí)行不會(huì)產(chǎn)生導(dǎo)致 UI 凍結(jié)的風(fēng)險(xiǎn)的小的、定義完善的操作。 |
在下列情況下,使用 UI 線程中的異步調(diào)用:
? |
執(zhí)行不影響 UI 的后臺(tái)操作。 |
? |
調(diào)用位于網(wǎng)絡(luò)的其他系統(tǒng)或資源。 |
? |
執(zhí)行可能花費(fèi)很長時(shí)間才能完成的操作。 |
前臺(tái)線程和后臺(tái)線程之間的選擇
.NET Framework 中的所有線程都被指定為前臺(tái)線程或后臺(tái)線程。這兩種線程唯一的區(qū)別是 — 后臺(tái)線程不會(huì)阻止進(jìn)程終止。在屬于一個(gè)進(jìn)程的所有前臺(tái)線程終止之后,公共語言運(yùn)行庫 (CLR) 就會(huì)結(jié)束進(jìn)程,從而終止仍在運(yùn)行的任何后臺(tái)線程。
在默認(rèn)情況下,通過創(chuàng)建并啟動(dòng)新的 Thread 對象生成的所有線程都是前臺(tái)線程,而從非托管代碼進(jìn)入托管執(zhí)行環(huán)境中的所有線程都標(biāo)記為后臺(tái)線程。然而,通過修改 Thread.IsBackground 屬性,可以指定一個(gè)線程是前臺(tái)線程還是后臺(tái)線程。通過將 Thread.IsBackground 設(shè)置為 true,可以將一個(gè)線程指定為后臺(tái)線程;通過將 Thread.IsBackground 設(shè)置為 false,可以將一個(gè)線程指定為前臺(tái)線程。
注有關(guān) Thread 對象的詳細(xì)信息,請參閱本章后面的“使用 Thread 類”部分。
在大多數(shù)應(yīng)用程序中,您會(huì)選擇將不同的線程設(shè)置成前臺(tái)線程或后臺(tái)線程。通常,應(yīng)該將被動(dòng)偵聽活動(dòng)的線程設(shè)置為后臺(tái)線程,而將負(fù)責(zé)發(fā)送數(shù)據(jù)的線程設(shè)置為前臺(tái)線程,這樣,在所有的數(shù)據(jù)發(fā)送完畢之前該線程不會(huì)被終止。
只有在確認(rèn)線程被系統(tǒng)隨意終止沒有不利影響時(shí),才應(yīng)該使用后臺(tái)線程。如果線程正在執(zhí)行必須完成的敏感操作或事務(wù)操作,或者需要控制關(guān)閉線程的方式以便釋放重要資源,則使用前臺(tái)線程。
處理鎖定和同步
有時(shí)在構(gòu)建應(yīng)用程序時(shí),創(chuàng)建的多個(gè)線程都需要同時(shí)使用關(guān)鍵資源(例如數(shù)據(jù)或應(yīng)用程序組件)。如果不仔細(xì),一個(gè)線程就可能更改另一個(gè)線程正在使用的資源。其結(jié)果可能就是該資源處于一種不確定的狀態(tài)并且呈現(xiàn)為不可用。這稱為 爭用情形。在沒有仔細(xì)考慮共享資源使用的情況下使用多線程的其他不利影響包括:死鎖、線程饑餓和線程關(guān)系問題。
為了防止這些影響,當(dāng)從兩個(gè)或多個(gè)線程訪問一個(gè)資源時(shí),需要使用鎖定和同步技術(shù)來協(xié)調(diào)這些嘗試訪問此資源的線程。
使用鎖定和同步來管理線程訪問共享資源是一項(xiàng)復(fù)雜的任務(wù),只要有可能,就應(yīng)該通過在線程之間傳送數(shù)據(jù)而不是提供對單個(gè)實(shí)例的共享訪問來避免這樣做。
假如不能排除線程之間的資源共享,則應(yīng)該:
? |
使用 Microsoft Visual C# 中的 lock 語句和 Microsoft Visual Basic .NET 中的 SyncLock 語句來創(chuàng)建臨界區(qū),但要小心地從臨界區(qū)內(nèi)調(diào)用方法來防止死鎖。 |
? |
使用 Synchronized 方法獲得線程安全的 .NET 集合。 |
? |
使用 ThreadStatic 屬性創(chuàng)建逐線程成員。 |
? |
使用重新檢查 (double-check) 鎖或 Interlocked.CompareExchange 方法來防止不必要的鎖定。 |
? |
確保靜態(tài)聲明是線程安全的。 |
有關(guān)鎖定和同步技術(shù)的詳細(xì)信息,請參閱 http://msdn.microsoft.com/library/en-us/cpgenref/html/cpconthreadingdesignguidelines.asp 上的 .NET Framework General Reference 中的“Threading Design Guidelines”。
使用計(jì)時(shí)器
在某些情況下,可能不需要使用單獨(dú)的線程。如果應(yīng)用程序需要定期執(zhí)行簡單的與 UI 有關(guān)的操作,則應(yīng)該考慮使用進(jìn)程計(jì)時(shí)器。有時(shí),在智能客戶端應(yīng)用程序中使用進(jìn)程計(jì)時(shí)器,以達(dá)到下列目:
? |
按計(jì)劃定期執(zhí)行操作。 |
? |
在使用圖形時(shí)保持一致的動(dòng)畫速度(而不管處理器的速度)。 |
? |
監(jiān)視服務(wù)器和其他的應(yīng)用程序以確認(rèn)它們在線并且正在運(yùn)行。 |
.NET Framework 提供三種進(jìn)程計(jì)時(shí)器:
? |
System.Window.Forms.Timer |
? |
System.Timers.Timer |
? |
System.Threading.Timer |
如果想要在 Windows 窗體應(yīng)用程序中引發(fā)事件,System.Window.Forms.Timer 就非常有用。它經(jīng)過了專門的優(yōu)化以便與 Windows 窗體一起使用,并且必須用在 Windows 窗體中。它設(shè)計(jì)成能用于單線程環(huán)境,并且可以在 UI 線程上同步操作。這就意味著該計(jì)時(shí)器從來不會(huì)搶占應(yīng)用程序代碼的執(zhí)行(假定沒有調(diào)用 Application.DoEvents),并且對與 UI 交互是安全的。
System.Timers.Timer 被設(shè)計(jì)并優(yōu)化成能用于多線程環(huán)境。與 System.Window.Forms.Timer 不同,此計(jì)時(shí)器調(diào)用從 CLR 線程池中獲得的輔助線程上的事件處理程序。在這種情況下,應(yīng)該確保事件處理程序不與 UI 交互。System.Timers.Timer 公開了可以模擬 System.Windows.Forms.Timer 中的行為的 SynchronizingObject 屬性,但是除非需要對事件的時(shí)間安排進(jìn)行更精確的控制,否則還是應(yīng)該改為使用 System.Windows.Forms.Timer。
System.Threading.Timer 是一個(gè)簡單的輕量級服務(wù)器端計(jì)時(shí)器。它并不是內(nèi)在線程安全的,并且使用起來比其他計(jì)時(shí)器更麻煩。此計(jì)時(shí)器通常不適合 Windows 窗體環(huán)境。
表 6.1 列出了每個(gè)計(jì)時(shí)器的各種屬性。
表 6.1 進(jìn)程計(jì)時(shí)器屬性
屬性 | System.Windows.Forms | System.Timers | System.Threading |
計(jì)時(shí)器事件運(yùn)行在什么線程中? |
UI 線程 |
UI 線程或輔助線程 |
輔助線程 |
實(shí)例是線程安全的嗎? |
否 |
是 |
否 |
需要 Windows 窗體嗎? |
是 |
否 |
否 |
最初的計(jì)時(shí)器事件可以調(diào)度嗎? |
否 |
否 |
是 |
何時(shí)使用多線程
在許多常見的情況下,可以使用多線程處理來顯著提高應(yīng)用程序的響應(yīng)能力和可用性。
應(yīng)該慎重考慮使用多線程來:
? |
通過網(wǎng)絡(luò)(例如,與 Web 服務(wù)器、數(shù)據(jù)庫或遠(yuǎn)程對象)進(jìn)行通信。 |
? |
執(zhí)行需要較長時(shí)間因而可能導(dǎo)致 UI 凍結(jié)的本地操作。 |
? |
區(qū)分各種優(yōu)先級的任務(wù)。 |
? |
提高應(yīng)用程序啟動(dòng)和初始化的性能。 |
非常詳細(xì)地分析這些使用情況是非常有用的。
通過網(wǎng)絡(luò)進(jìn)行通信
智能客戶端可以采用許多方式通過網(wǎng)絡(luò)進(jìn)行通信,其中包括:
? |
遠(yuǎn)程對象調(diào)用,例如,DCOM、RPC 或 .NET 遠(yuǎn)程處理 |
? |
基于消息的通信,例如,Web 服務(wù)調(diào)用和 HTTP 請求。 |
? |
分布式事務(wù)處理。 |
許多因素決定了網(wǎng)絡(luò)服務(wù)對應(yīng)用程序請求的響應(yīng)速度,其中包括請求的性質(zhì)、網(wǎng)絡(luò)滯后時(shí)間、連接的可靠性和帶寬、單個(gè)服務(wù)或多個(gè)服務(wù)的繁忙程度。
這種不可預(yù)測性可能會(huì)引起單線程應(yīng)用程序的響應(yīng)問題,而多線程處理常常是一種好的解決方案。應(yīng)該為網(wǎng)絡(luò)上的所有通信創(chuàng)建針對 UI 線程的單獨(dú)線程,然后在接收到響應(yīng)時(shí)將數(shù)據(jù)傳送回 UI 線程。
為網(wǎng)絡(luò)通信創(chuàng)建單獨(dú)的線程并不總是必要的。如果應(yīng)用程序通過網(wǎng)絡(luò)進(jìn)行異步通信,例如使用 Microsoft Windows 消息隊(duì)列(也稱為 MSMQ),則在繼續(xù)執(zhí)行之前,它不會(huì)等待響應(yīng)。然而,即使在這種情況下,您仍然應(yīng)該使用單獨(dú)的線程來偵聽響應(yīng),并且在響應(yīng)到達(dá)時(shí)對其進(jìn)行處理。
執(zhí)行本地操作
即使在處理發(fā)生在本地的情況下,有些操作也可能花費(fèi)很長時(shí)間,足以對應(yīng)用程序的響應(yīng)產(chǎn)生負(fù)面影響。這樣的操作包括:
? |
圖像呈現(xiàn)。 |
? |
數(shù)據(jù)操縱。 |
? |
數(shù)據(jù)排序。 |
? |
搜索。 |
不應(yīng)該在 UI 線程上執(zhí)行諸如此類的操作,因?yàn)檫@樣做會(huì)引起應(yīng)用程序中的性能問題。相反,應(yīng)該使用額外的線程來異步執(zhí)行這些操作,防止 UI 線程阻塞。
在許多情況下,也應(yīng)該這樣設(shè)計(jì)應(yīng)用程序,讓它報(bào)告正在進(jìn)行的后臺(tái)操作的進(jìn)程和成功或失敗。可能還會(huì)考慮允許用戶取消后臺(tái)操作以提高可用性。
區(qū)分各種優(yōu)先級的任務(wù)
并不是應(yīng)用程序必須執(zhí)行的所有任務(wù)都具有相同的優(yōu)先級。一些任務(wù)對時(shí)間要求很急,而一些則不是。在其他的情況中,您或許會(huì)發(fā)現(xiàn)一個(gè)線程依賴于另一個(gè)線程上的處理結(jié)果。
應(yīng)該創(chuàng)建不同優(yōu)先級的線程以反映正在執(zhí)行的任務(wù)的優(yōu)先級。例如,應(yīng)該使用高優(yōu)先級線程管理對時(shí)間要求很急的任務(wù),而使用低優(yōu)先級線程執(zhí)行被動(dòng)任務(wù)或者對時(shí)間不敏感的任務(wù)。
應(yīng)用程序啟動(dòng)
應(yīng)用程序在第一次運(yùn)行時(shí)常常必須執(zhí)行許多操作。例如,它可能需要初始化自己的狀態(tài),檢索或更新數(shù)據(jù),打開本地資源的連接。應(yīng)該考慮使用單獨(dú)的線程來初始化應(yīng)用程序,從而使得用戶能夠盡快地開始使用該應(yīng)用程序。使用單獨(dú)的線程進(jìn)行初始化可以增強(qiáng)應(yīng)用程序的響應(yīng)能力和可用性。
如果確實(shí)在單獨(dú)的線程中執(zhí)行初始化,則應(yīng)該通過在初始化完成之后,更新 UI 菜單和工具欄按鈕的狀態(tài)來防止用戶啟動(dòng)依賴于初始化尚未完成的操作。還應(yīng)該提供清楚的反饋消息來通知用戶初始化的進(jìn)度。
創(chuàng)建和使用線程
在 .NET Framework 中有幾種方法可以創(chuàng)建和使用后臺(tái)線程。可以使用 ThreadPool 類訪問由 .NET Framework 管理的給定進(jìn)程的線程池,也可以使用 Thread 類顯式地創(chuàng)建和管理線程。另外,還可以選擇使用委托對象或者 Web 服務(wù)代理來使非 UI 線程上發(fā)生特定處理。本節(jié)將依次分析各種不同的方法,并推薦每種方法應(yīng)該在何時(shí)使用。
使用 ThreadPool 類
到現(xiàn)在為止,您可能會(huì)認(rèn)識(shí)到許多應(yīng)用程序都會(huì)從多線程處理中受益。然而,線程管理并不僅僅是每次想要執(zhí)行一個(gè)不同的任務(wù)就創(chuàng)建一個(gè)新線程的問題。有太多的線程可能會(huì)使得應(yīng)用程序耗費(fèi)一些不必要的系統(tǒng)資源,特別是,如果有大量短期運(yùn)行的操作,而所有這些操作都運(yùn)行在單獨(dú)線程上。另外,顯式地管理大量的線程可能是非常復(fù)雜的。
線程池化技術(shù)通過給應(yīng)用程序提供由系統(tǒng)管理的輔助線程池解決了這些問題,從而使得您可以將注意力集中在應(yīng)用程序任務(wù)上而不是線程管理上。
在需要時(shí),可以由應(yīng)用程序?qū)⒕€程添加到線程池中。當(dāng) CLR 最初啟動(dòng)時(shí),線程池沒有包含額外的線程。然而,當(dāng)應(yīng)用程序請求線程時(shí),它們就會(huì)被動(dòng)態(tài)創(chuàng)建并存儲(chǔ)在該池中。如果線程在一段時(shí)間內(nèi)沒有使用,這些線程就可能會(huì)被處置,因此線程池是根據(jù)應(yīng)用程序的要求縮小或擴(kuò)大的。
注每個(gè)進(jìn)程都創(chuàng)建一個(gè)線程池,因此,如果您在同一個(gè)進(jìn)程內(nèi)運(yùn)行幾個(gè)應(yīng)用程序域,則一個(gè)應(yīng)用程序域中的錯(cuò)誤可能會(huì)影響相同進(jìn)程內(nèi)的其他應(yīng)用程序域,因?yàn)樗鼈兌际褂孟嗤木€程池。
線程池由兩種類型的線程組成:
? |
輔助線程。輔助線程是標(biāo)準(zhǔn)系統(tǒng)池的一部分。它們是由 .NET Framework 管理的標(biāo)準(zhǔn)線程,大多數(shù)功能都在它們上面執(zhí)行。 |
? |
完成端口線程.這種線程用于異步 I/O 操作(通過使用 IOCompletionPorts API)。 |
注,如果應(yīng)用程序嘗試在沒有 IOCompletionPorts 功能的計(jì)算機(jī)上執(zhí)行 I/O 操作,它就會(huì)還原到使用輔助線程。
對于每個(gè)計(jì)算機(jī)處理器,線程池都默認(rèn)包含 25 個(gè)線程。如果所有的 25 個(gè)線程都在被使用,則附加的請求將排入隊(duì)列,直到有一個(gè)線程變得可用為止。每個(gè)線程都使用默認(rèn)堆棧大小,并按默認(rèn)的優(yōu)先級運(yùn)行。
下面代碼示例說明了線程池的使用。
private void ThreadPoolExample() { WaitCallback callback = new WaitCallback( ThreadProc ); ThreadPool.QueueUserWorkItem( callback ); }
在前面的代碼中,首先創(chuàng)建一個(gè)委托來引用您想要在輔助線程中執(zhí)行的代碼。.NET Framework 定義了 WaitCallback 委托,該委托引用的方法接受一個(gè)對象參數(shù)并且沒有返回值。下面的方法實(shí)現(xiàn)您想要執(zhí)行的代碼。
private void ThreadProc( Object stateInfo ) { // Do something on worker thread. }
可以將單個(gè)對象參數(shù)傳遞給 ThreadProc 方法,方法是將其指定為 QueueUserWorkItem 方法調(diào)用中的第二個(gè)參數(shù)。在前面的示例中,沒有給 ThreadProc 方法傳遞參數(shù),因此 stateInfo 參數(shù)為空。
在下面的情況下,使用 ThreadPool 類:
? |
有大量小的獨(dú)立任務(wù)要在后臺(tái)執(zhí)行。 |
? |
不需要對用來執(zhí)行任務(wù)的線程進(jìn)行精細(xì)控制。 |
使用 Thread 類
使用 Thread 類可以顯式管理線程。這包括 CLR 創(chuàng)建的線程和進(jìn)入托管環(huán)境執(zhí)行代碼的 CLR 以外創(chuàng)建的線程。CLR 監(jiān)視其進(jìn)程中曾經(jīng)在 .NET Framework 內(nèi)執(zhí)行代碼的所有線程,并且使用 Thread 類的實(shí)例來管理它們。
只要有可能,就應(yīng)該使用 ThreadPool 類來創(chuàng)建線程。然而,在一些情況下,您還是需要?jiǎng)?chuàng)建并管理您自己的線程,而不是使用 ThreadPool 類。
在下面的情況下,使用 Thread 對象:
? |
需要具有特定優(yōu)先級的任務(wù)。 |
? |
有可能運(yùn)行很長時(shí)間的任務(wù)(這樣可能阻塞其他任務(wù))。 |
? |
需要確保只有一個(gè)線程可以訪問特定的程序集。 |
? |
需要有與線程相關(guān)的穩(wěn)定標(biāo)識(shí)。 |
Thread 對象包括許多屬性和方法,它們可以幫助控制線程。可以設(shè)置線程的優(yōu)先級,查詢當(dāng)前的線程狀態(tài),中止線程,臨時(shí)阻塞線程,并且執(zhí)行許多其他的線程管理任務(wù)。
下面的代碼示例演示了如何使用 Thread 對象創(chuàng)建并啟動(dòng)一個(gè)線程。
static void Main() { System.Threading.Thread workerThread = new System.Threading.Thread( SomeDelegate ); workerThread.Start(); } public static void SomeDelegate () { Console.WriteLine( "Do some work." ); }
在這個(gè)示例中,SomeDelegate 是一個(gè) ThreadStart 委托 — 指向?qū)⒁谛戮€程中執(zhí)行的代碼的引用。Thread.Start 向操作系統(tǒng)提交請求以啟動(dòng)線程。
如果采用這種方式實(shí)例化一個(gè)新線程,就不可能向 ThreadStart 委托傳遞任何參數(shù)。如果需要將一個(gè)參數(shù)傳遞給要在另一個(gè)線程中執(zhí)行的方法,應(yīng)該用所需的方法簽名創(chuàng)建一個(gè)自定義委托并異步調(diào)用它。
有關(guān)自定義委托的詳細(xì)信息,請參閱本章后面的“使用委托”部分。
如果需要從單獨(dú)的線程中獲得更新或結(jié)果,可以使用回調(diào)方法 — 一個(gè)委托,引用在線程完成工作之后將要調(diào)用的代碼 — 這就使得線程可以與 UI 交互。有關(guān)詳細(xì)信息,請參閱本章后面的“使用任務(wù)處理 UI 線程和其他線程之間的交互”部分。
使用委托
委托是指向方法的引用(或指針)。在定義委托時(shí),可以指定確切的方法簽名,如果其他的方法想要代表該委托,就必須與該簽名相匹配。所有委托都可以同步和異步調(diào)用。
下面的代碼示例展示了如何聲明委托。這個(gè)示例展示了如何將一個(gè)長期運(yùn)行的計(jì)算實(shí)現(xiàn)為一個(gè)類中的方法。
delegate string LongCalculationDelegate( int count );
如果 .NET Framework 遇到像上面一樣的委托聲明,就隱式聲明了一個(gè)從 MultiCastDelegate 類繼承的隱藏類,正如下面的代碼示例中所示。
Class LongCalculationDelegate : MutlicastDelegate { public string Invoke( count ); public void BeginInvoke( int count, AsyncCallback callback, object asyncState ); public string EndInvoke( IAsyncResult result ); }
委托類型 LongCalculationDelegate 用于引用接受單個(gè)整型參數(shù)并返回一個(gè)字符串的方法。下面的代碼示例舉例說明了一個(gè)這種類型的委托,它引用帶有相關(guān)簽名的特定方法。
LongCalculationDelegate longCalcDelegate = new LongCalculationDelegate( calculationMethod );
在本示例中,calculationMethod 是實(shí)現(xiàn)您想要在單獨(dú)線程上執(zhí)行的計(jì)算的方法的名稱。
可以同步或異步調(diào)用委托實(shí)例所引用的方法。為了同步調(diào)用它,可以使用下面的代碼。
string result = longCalcDelegate( 10000 );
該代碼在內(nèi)部使用上面的委托類型中定義的 Invoke 方法。因?yàn)?Invoke 方法是同步調(diào)用,所以此方法只在調(diào)用方法返回之后才返回。返回值是調(diào)用方法的結(jié)果。
更常見的情況是,為了防止調(diào)用線程阻塞,您將選擇通過使用 BeginInvoke 和 B>EndInvoke 方法來異步調(diào)用委托。異步委托使用 .NET Framework 中的線程池化功能來進(jìn)行線程管理。.NET Framework 實(shí)現(xiàn)的標(biāo)準(zhǔn)異步調(diào)用 模式提供 BeginInvoke 方法來啟動(dòng)線程上所需的操作,并且它提供 EndInvoke 方法來允許完成異步操作以及將任何得到的數(shù)據(jù)傳送回調(diào)用線程。在后臺(tái)處理完成之后,可以調(diào)用回調(diào)方法,其中,可以調(diào)用 EndInvoke 來獲取異步操作的結(jié)果。
當(dāng)調(diào)用 BeginInvoke 方法時(shí),它不會(huì)等待調(diào)用完成;相反,它會(huì)立即返回一個(gè) IAsyncResult 對象,該對象可以用來監(jiān)視該調(diào)用的進(jìn)度。可以使用 IAsyncResult 對象的 WaitHandle 成員來等待異步調(diào)用完成,或使用 IsComplete 成員輪詢是否完成。如果在調(diào)用完成之前調(diào)用 EndInvoke 方法,它就會(huì)阻塞,并且只在調(diào)用完成之后才返回。然而,您應(yīng)該慎重,不要使用這些技術(shù)來等待調(diào)用完成,因?yàn)樗鼈兛赡茏枞?UI 線程。一般來說,回調(diào)機(jī)制是通知調(diào)用已經(jīng)完成的最好方式。
異步執(zhí)行委托引用的方法
1. |
定義代表長期運(yùn)行的異步操作的委托,如下面的示例所示: delegate string LongCalculationDelegate( int count ); |
2. |
定義一個(gè)與委托簽名相匹配的方法。下面的示例中的方法模擬需要消耗較多時(shí)間的操作,方法是使線程返回之前睡眠 count 毫秒。 private string LongCalculation( int count ) { Thread.Sleep( count ); return count.ToString(); } |
3. |
定義與 .NET Framework 定義的 AsyncCallback 委托相對應(yīng)的回調(diào)方法,如下面的示例所示。 private void CallbackMethod( IAsyncResult ar ) { // Retrieve the invoking delegate. LongCalculationDelegate dlgt = (LongCalculationDelegate)ar.AsyncState; // Call EndInvoke to retrieve the results. string results = dlgt.EndInvoke(ar); } |
4. |
創(chuàng)建一個(gè)委托實(shí)例,它引用您想要異步調(diào)用的方法,并且創(chuàng)建一個(gè) AsyncCallback 委托來引用回調(diào)方法,如下面的代碼示例所示。 LongCalculationDelegate longCalcDelegate = new LongCalculationDelegate( calculationMethod ); AsyncCallback callback = new AsyncCallback( CallbackMethod ); |
5. |
從調(diào)用線程中開始異步調(diào)用,方法是調(diào)用引用您想要異步執(zhí)行的代碼的委托中的 BeginInvoke 方法。 longCalcDelegate.BeginInvoke( count, callback, longCalcDelegate ); 方法 LongCalculation 是在輔助線程上調(diào)用的。當(dāng)它完成時(shí),就調(diào)用 CallbackMethod 方法,并且獲取計(jì)算的結(jié)果。 注回調(diào)方法是在非 UI 線程上執(zhí)行的。要修改 UI,需要使用某些技術(shù)來從該線程切換到 UI 線程。有關(guān)詳細(xì)信息,請參閱本章后面的“使用任務(wù)處理 UI 線程和其他線程之間的交互”部分。 |
可以使用自定義委托來將任意參數(shù)傳送給要在單獨(dú)的線程上執(zhí)行的方法(有時(shí)當(dāng)您直接使用 Thread 對象或線程池創(chuàng)建線程時(shí),您無法這樣做)。
當(dāng)需要在應(yīng)用程序 UI 中調(diào)用長期運(yùn)行的操作時(shí),異步調(diào)用委托非常有用。如果用戶在 UI 中執(zhí)行預(yù)期要花很長時(shí)間才能完成的操作,您肯定并不希望該 UI 凍結(jié),也不希望它不能刷新自己。使用異步委托,可以將控制權(quán)返回給主 UI 線程以執(zhí)行其他操作。
在以下情況中,您應(yīng)該使用委托來異步調(diào)用方法:
? |
需要將任意參數(shù)傳遞給您想要異步執(zhí)行的方法。 |
? |
您想要使用 .NET Framework 提供的異步調(diào)用 模式。 |
注有關(guān)如何使用 BeginInvoke 和 EndInvoke 進(jìn)行異步調(diào)用的詳細(xì)信息,請參閱http://msdn.microsoft.com/library/en-us/cpguide/html/cpovrasynchronousprogrammingoverview.asp 上的 .NET Framework Developer's Guide 中的“Asynchronous Programming Overview”。
異步調(diào)用 Web 服務(wù)
應(yīng)用程序常常使用 Web 服務(wù)與網(wǎng)絡(luò)資源進(jìn)行通信。一般來說,不應(yīng)該從 UI 線程同步調(diào)用 Web 服務(wù),這是因?yàn)?Web 服務(wù)調(diào)用的響應(yīng)時(shí)間變化很大,正如網(wǎng)絡(luò)上所有交互的響應(yīng)時(shí)間的情況一樣。相反,應(yīng)該從客戶端異步調(diào)用所有的 Web 服務(wù)。
要了解如何異步調(diào)用 Web 服務(wù),可以考慮使用下面簡單的 Web 服務(wù),它睡眠一段時(shí)間,然后返回一個(gè)字符串,指示它已經(jīng)完成了它的操作。
[WebMethod] public string ReturnMessageAfterDelay( int delay ) { System.Threading.Thread.Sleep(delay); return "Message Received"; }
當(dāng)在 Microsoft Visual Studio .NET 開發(fā)系統(tǒng)中引用 Web 服務(wù)時(shí),它會(huì)自動(dòng)生成一個(gè)代理。代理是一個(gè)類,它允許使用 .NET Framework 實(shí)現(xiàn)的異步調(diào)用 模式異步調(diào)用 Web 服務(wù)。如果您分析一下生成的代理,您就會(huì)看到下面三個(gè)方法。
public string ReturnMessageAfterDelay( int delay ) { object[] results = this.Invoke( "ReturnMessageAfterDelay", new object[] {delay} ); return ((string)(results[0])); } public System.IAsyncResult BeginReturnMessageAfterDelay( int delay, System.AsyncCallback callback, object asyncState ) { return this.BeginInvoke( "ReturnMessageAfterDelay", new object[] {delay}, callback, asyncState ); } public string EndReturnMessageAfterDelay( System.IAsyncResult asyncResult ) { object[] results = this.EndInvoke( asyncResult ); return ((string)(results[0])); }
第一個(gè)方法是調(diào)用 Web 服務(wù)的同步方法。第二個(gè)和第三個(gè)方法是異步方法。可以如下所示異步調(diào)用 Web 服務(wù)。
private void CallWebService() { localhost.LongRunningService serviceProxy = new localhost.LongRunningService(); AsyncCallback callback = new AsyncCallback( Completed ); serviceProxy.BeginReturnMessageAfterDelay( callback, serviceProxy, null ); }
這個(gè)示例非常類似于使用自定義委托的異步回調(diào)示例。當(dāng) Web 服務(wù)返回時(shí),用將要調(diào)用的方法定義一個(gè) AsyncCallback 對象。用指定回調(diào)和代理本身的方法調(diào)用異步 Web 服務(wù),正如下面的代碼示例所示:
void Completed( IAsyncResult ar ) { localhost.LongRunningService serviceProxy = (localhost.LongRunningService)ar.AsyncState; string message = serviceProxy.EndReturnMessageAfterDelay( ar ); }
當(dāng) Web 服務(wù)完成時(shí),就調(diào)用完成的回調(diào)方法。然后,可以通過調(diào)用代理上的 EndReturnMessageAfterDelay 來獲取異步結(jié)果。
使用任務(wù)處理 UI 線程和其他線程之間的交互
設(shè)計(jì)多線程應(yīng)用程序最復(fù)雜的一個(gè)方面是處理 UI 線程和其他線程之間的關(guān)系。用于應(yīng)用程序的后臺(tái)線程并不直接與應(yīng)用程序 UI 交互,這一點(diǎn)相當(dāng)關(guān)鍵。如果后臺(tái)線程試圖修改應(yīng)用程序的 UI 中的控件,該控件就可能會(huì)處于一種未知的狀態(tài)。這可能會(huì)在應(yīng)用程序中引起較大的問題,并難于診斷。例如,當(dāng)另一個(gè)線程正在給動(dòng)態(tài)生成的位圖傳送新數(shù)據(jù)時(shí),它或許不能顯示。或者,當(dāng)數(shù)據(jù)集正在刷新時(shí),綁定到數(shù)據(jù)集的組件可能會(huì)顯示沖突信息。
為了避免這些問題,應(yīng)該從不允許 UI 線程以外的線程更改 UI 控件或綁定到 UI 的數(shù)據(jù)對象。您應(yīng)該始終盡力維護(hù) UI 代碼和后臺(tái)處理代碼之間的嚴(yán)格分離。
將 UI 線程與其他線程分離是一個(gè)良好的做法,但是您仍然需要在這些線程之間來回傳遞信息。多線程應(yīng)用程序通常需要具有下列功能:
? |
從后臺(tái)線程獲得結(jié)果并更新 UI。 |
? |
當(dāng)后臺(tái)線程執(zhí)行它的處理時(shí)向 UI 報(bào)告進(jìn)度。 |
? |
從 UI 控制后臺(tái)線程,例如讓用戶取消后臺(tái)處理。 |
從處理后臺(tái)線程的代碼中分離 UI 代碼的有效方法是,根據(jù)任務(wù)構(gòu)造應(yīng)用程序,并且使用封裝所有任務(wù)細(xì)節(jié)的對象代表每個(gè)任務(wù)。
任務(wù)是用戶期望能夠在應(yīng)用程序內(nèi)完成的一個(gè)工作單元。在多線程處理的環(huán)境中,Task 對象封裝了所有的線程細(xì)節(jié),這樣它們就可以從 UI 中清晰地分離出來。
通過使用 Task 模式,在使用多線程時(shí)可以簡化代碼。Task 模式將線程管理代碼從 UI 代碼中清晰地分離出來。UI 使用 Task 對象提供的屬性和方法來執(zhí)行行動(dòng),比如啟動(dòng)和停止任務(wù)、以及查詢它們的狀態(tài)。Task 對象也可以提供許多事件,從而允許將狀態(tài)信息傳送回 UI。這些事件都應(yīng)該在 UI 線程內(nèi)激發(fā),這樣,UI 就不需要了解后臺(tái)線程。
使用 Task 對象可以充分簡化線程交互,Task 對象雖然負(fù)責(zé)控制和管理后臺(tái)線程,但是激發(fā) UI 可以使用并且保證在 UI 線程上的事件。Task 對象可以在應(yīng)用程序的各個(gè)部分中重用,甚至也可以在其他的應(yīng)用程序中重用。
圖 6.1 說明了使用 Task 模式時(shí)代碼的整體結(jié)構(gòu)。

圖 6.1 使用 Task 模式時(shí)的代碼結(jié)構(gòu)
注Task 模式可以用來在單獨(dú)的線程上執(zhí)行本地后臺(tái)處理任務(wù),或者與網(wǎng)絡(luò)上的遠(yuǎn)程服務(wù)異步交互。在后者的情況下,Task 對象常常稱為服務(wù)代理。服務(wù)代理可以使用與 Task 對象相同的模式,并且可以支持使其與 UI 交互更容易的屬性和事件。
因?yàn)?Task 對象封裝了任務(wù)的狀態(tài),所以可以用它來更新 UI。要這樣做,無論何時(shí)發(fā)生更改,都可以讓 Task 對象針對主 UI 線程激發(fā) PropertyChanged 事件。這些事件提供一種標(biāo)準(zhǔn)而一致的方法來傳遞屬性值更改。
可以使用任務(wù)來通知主 UI 線程進(jìn)度或其他狀態(tài)改變。例如,當(dāng)任務(wù)變得可用時(shí),可以將其設(shè)置為已啟用的標(biāo)志,該標(biāo)志可用于啟用相應(yīng)的菜單項(xiàng)和工具欄按鈕。相反,當(dāng)任務(wù)變得不可用(例如,因?yàn)樗€在進(jìn)行中),可以將已啟用標(biāo)志設(shè)置為 false,這會(huì)導(dǎo)致主 UI 線程中的事件處理程序禁用適當(dāng)?shù)牟藛雾?xiàng)和工具欄按鈕。
也可以使用任務(wù)來更新綁定到 UI 的數(shù)據(jù)對象。應(yīng)該確保數(shù)據(jù)綁定到 UI 控件的任何數(shù)據(jù)對象在 UI 線程上更新。例如,如果將 DataSet 對象綁定到 UI 并從 Web 服務(wù)檢索更新信息,就可以將新數(shù)據(jù)傳遞給 UI 代碼。然后,UI 代碼將新數(shù)據(jù)合并到 UI 線程上綁定的 DataSet 對象中。
可以使用 Task 對象實(shí)現(xiàn)后臺(tái)處理和線程控制邏輯。因?yàn)?Task 對象封裝了必要的狀態(tài)和數(shù)據(jù),所以它可以協(xié)調(diào)在一個(gè)或更多線程中完成任務(wù)所需的工作,并且在需要時(shí)傳遞更改和通知到應(yīng)用程序的 UI。可以實(shí)現(xiàn)所有必需的鎖定和同步并將其封裝在 Task 對象中,這樣 UI 線程就不必處理這些問題。
定義 Task 類
下面的代碼示例顯示了管理長期計(jì)算任務(wù)的類定義。
注雖然該示例比較簡單,但是它可以很容易地?cái)U(kuò)展為支持在應(yīng)用程序的 UI 中集成的復(fù)雜后臺(tái)任務(wù)。
public class CalculationTask { // Class Members… public CalculationTask(); public void StartCalculation( int count ); public void StopCalculation(); private void FireStatusChangedEvent( CalculationStatus status ); private void FireProgressChangedEvent( int progress ); private string Calculate( int count ); private void EndCalculate( IAsyncResult ar ); }
CalculationTask 類定義一個(gè)默認(rèn)的構(gòu)造函數(shù)和兩個(gè)公共方法來啟動(dòng)和停止計(jì)算。它還定義了幫助器方法來幫助 Task 對象激發(fā)針對 UI 的事件。Calculate 方法實(shí)現(xiàn)計(jì)算邏輯,并且運(yùn)行在后臺(tái)線程上。EndCalculate 方法實(shí)現(xiàn)回調(diào)方法,它是在后臺(tái)計(jì)算線程完成之后調(diào)用的。
類成員如下:
private CalculationStatus _calcState; private delegate string CalculationDelegate( int count ); public delegate void CalculationStatusEventHandler( object sender, CalculationEventArgs e ); public delegate void CalculationProgressEventHandler( object sender, CalculationEventArgs e ); public event CalculationStatusEventHandler CalculationStatusChanged; public event CalculationProgressEventHandler CalculationProgressChanged;
CalculationStatus 成員是一個(gè)枚舉,它定義了在任何一個(gè)時(shí)刻計(jì)算可能處于的三個(gè)狀態(tài)。
public enum CalculationStatus { NotCalculating, Calculating, CancelPending }
Task 類提供兩個(gè)事件:一個(gè)通知 UI 有關(guān)計(jì)算狀態(tài)的事件,另一個(gè)通知 UI 有關(guān)計(jì)算進(jìn)度的事件。委托簽名與事件本身都要定義。
這兩個(gè)事件是在幫助器方法中激發(fā)的。這些方法檢查目標(biāo)的類型,如果目標(biāo)類型是從 Control 類派生的,它們就使用 Control 類中的 Invoke 方法來激發(fā)事件。因此,對于 UI 事件接收器,可以保證事件是在 UI 線程上調(diào)用的。下面的示例展示了激發(fā)事件的代碼。
private void FireStatusChangedEvent( CalculationStatus status ) { if( CalculationStatusChanged != null ) { CalculationEventArgs args = new CalculationEventArgs( status ); if ( CalculationStatusChanged.Target is System.Windows.Forms.Control ) { Control targetForm = CalculationStatusChanged.Target as System.Windows.Forms.Control; targetForm.Invoke( CalculationStatusChanged, new object[] { this, args } ); } else { CalculationStatusChanged( this, args ); } } }
這段代碼首先檢查事件接收器是否已經(jīng)注冊,如果它已經(jīng)注冊,就檢查目標(biāo)的類型。如果目標(biāo)類型是從 Control 類派生的,就使用 Invoke 方法激發(fā)該事件以確保在 UI 線程上處理它。如果目標(biāo)類型不是從 Control 類派生的,就正常激發(fā)事件。在 FireProgressChangedEvent 方法中,以相同的方式激發(fā)事件以向 UI 報(bào)告計(jì)算進(jìn)度,如下列的示例所示。
private void FireProgressChangedEvent( int progress ) { if( CalculationProgressChanged != null ) { CalculationEventArgs args = new CalculationEventArgs( progress ); if ( CalculationStatusChanged.Target is System.Windows.Forms.Control ) { Control targetForm = CalculationStatusChanged.Target as System.Windows.Forms.Control; targetForm.Invoke( CalculationProgressChanged, new object[] { this, args } ); } else { CalculationProgressChanged( this, args ); } } }
CalculationEventArgs 類定義了兩個(gè)事件的事件參數(shù),并且包含計(jì)算狀態(tài)和進(jìn)度參數(shù),以便將它們發(fā)送給 UI。CalculationEventArgs 類的定義如下所示。
public class CalculationEventArgs : EventArgs { public string Result; public int Progress; public CalculationStatus Status; public CalculationEventArgs( int progress ) { this.Progress = progress; this.Status = CalculationStatus.Calculating; } public CalculationEventArgs( CalculationStatus status ) { this.Status = status; } }
StartCalculation 方法負(fù)責(zé)啟動(dòng)后臺(tái)線程上的計(jì)算。委托 CalculationDelegate 允許使用委托異步調(diào)用 (Delegate Asynchronous Call) 模式在后臺(tái)線程上調(diào)用 Calculation 方法,如下面的示例所示。
public void StartCalculation( int count ) { lock( this ) { if( _calcState == CalculationStatus.NotCalculating ) { // Create a delegate to the calculation method. CalculationDelegate calc = new CalculationDelegate( Calculation ); // Start the calculation. calc.BeginInvoke( count, new AsyncCallback( EndCalculate ), calc ); // Update the calculation status. _calcState = CalculationStatus.Calculating; // Fire a status changed event. FireStatusChangedEvent( _calcState ); } } }
StopCalculation 方法負(fù)責(zé)取消計(jì)算,如下面的代碼示例所示。
public void StopCalculation() { lock( this ) { if( _calcState == CalculationStatus.Calculating ) { // Update the calculation status. _calcState = CalculationStatus.CancelPending; // Fire a status changed event. FireStatusChangedEvent( _calcState ); } } }
當(dāng)調(diào)用 StopCalculation 時(shí),計(jì)算狀態(tài)被設(shè)置為 CancelPending,以通知后臺(tái)停止計(jì)算。向 UI 激發(fā)一個(gè)事件,以通知已經(jīng)接收到取消請求。
這兩個(gè)方法都使用 lock 關(guān)鍵字來確保對計(jì)算狀態(tài)變量的更改是原子的,這樣應(yīng)用程序就不會(huì)遇到爭用情形。這兩個(gè)方法都激發(fā)狀態(tài)改變事件來通知 UI 計(jì)算正在啟動(dòng)或停止。
計(jì)算方法定義如下。
private string Calculation( int count ) { string result = ""; for ( int i = 0 ; i < count ; i++ ) { // Long calculation… // Check for cancel. if ( _calcState == CalculationStatus.CancelPending ) break; // Update Progress FireProgressChangedEvent( count, i ); } return result; }
注為清楚起見,計(jì)算的細(xì)節(jié)已經(jīng)忽略。
每次傳遞都是通過循環(huán)進(jìn)行的,這樣就可以檢查計(jì)算狀態(tài)成員,以查看用戶是否已經(jīng)取消了計(jì)算。如果這樣,循環(huán)就退出,從而完成計(jì)算方法。如果計(jì)算繼續(xù)進(jìn)行,就使用 FireProgressChanged 幫助器方法來激發(fā)事件,以向 UI 報(bào)告進(jìn)度。
在計(jì)算完成之后,就調(diào)用 EndCalculate 方法,以便通過調(diào)用 EndInvoke 方法來完成異步調(diào)用,如下面的示例所示。
private void EndCalculate( IAsyncResult ar ) { CalculationDelegate del = (CalculationDelegate)ar.AsyncState; string result = del.EndInvoke( ar ); lock( this ) { _calcState = CalculationStatus.NotCalculating; FireStatusChangedEvent( _calcState ); } }
EndCalculate 將計(jì)算狀態(tài)重置為 NotCalculating,準(zhǔn)備開始下一次計(jì)算。同時(shí),它激發(fā)一個(gè)狀態(tài)改變事件,這樣就可以通知 UI 計(jì)算已經(jīng)完成。
使用 Task 類
Task 類負(fù)責(zé)管理后臺(tái)線程。要使用 Task 類,必須做的事情就是創(chuàng)建一個(gè) Task 對象,注冊它激發(fā)的事件,并且實(shí)現(xiàn)這些事件的處理。因?yàn)槭录窃?UI 線程上激發(fā)的,所以您根本不必?fù)?dān)心代碼中的線程處理問題。
下面的示例展示了如何創(chuàng)建 Task 對象。在這個(gè)示例中,UI 有兩個(gè)按鈕,一個(gè)用于啟動(dòng)計(jì)算,一個(gè)用于停止計(jì)算,還有一個(gè)進(jìn)度欄顯示當(dāng)前的計(jì)算進(jìn)度。
// Create new task object to manage the calculation. _calculationTask = new CalculationTask(); // Subscribe to the calculation status event. _ calculationTask.CalculationStatusChanged += new CalculationTask.CalculationStatusEventHandler( OnCalculationStatusChanged ); // Subscribe to the calculation progress event. _ calculationTask.CalculationProgressChanged += new CalculationTask.CalculationProgressEventHandler( OnCalculationProgressChanged );
用于計(jì)算狀態(tài)和計(jì)算進(jìn)度事件的事件處理程序相應(yīng)地更新 UI,例如通過更新狀態(tài)欄控件。
private void CalculationProgressChanged( object sender, CalculationEventArgs e ) { _progressBar.Value = e.Progress; }
下面的代碼展示的 CalculationStatusChanged 事件處理程序更新進(jìn)度欄的值以反映當(dāng)前的計(jì)算進(jìn)度。假定進(jìn)度欄的最小值和最大值已經(jīng)初始化。
private void CalculationStatusChanged( object sender, CalculationEventArgs e ) { switch ( e.Status ) { case CalculationStatus.Calculating: button1.Enabled = false; button2.Enabled = true; break; case CalculationStatus.NotCalculating: button1.Enabled = true; button2.Enabled = false; break; case CalculationStatus.CancelPending: button1.Enabled = false; button2.Enabled = false; break; } }
在這個(gè)示例中,CalculationStatusChanged 事件處理程序根據(jù)計(jì)算狀態(tài)啟用和禁用啟動(dòng)和停止按鈕。這可以防止用戶嘗試啟動(dòng)一個(gè)已經(jīng)在進(jìn)行的計(jì)算,并且向用戶提供有關(guān)計(jì)算狀態(tài)的反饋。
通過使用 Task 對象中的公共方法,UI 為每個(gè)按鈕單擊實(shí)現(xiàn)了窗體事件處理程序,以便啟動(dòng)和停止計(jì)算。例如,啟動(dòng)按鈕事件處理程序調(diào)用 StartCalculation 方法,如下所示。
private void startButton_Click( object sender, System.EventArgs e ) { calculationTask.StartCalculation( 1000 ); }
類似地,停止計(jì)算按鈕通過調(diào)用 StopCalculation 方法來停止計(jì)算,如下所示。
private void stopButton_Click( object sender, System.EventArgs e ) { calculationTask.StopCalculation(); }
小結(jié)
多線程處理是創(chuàng)建可以響應(yīng)的智能客戶端應(yīng)用程序的重要部分。應(yīng)該分析多線程適合于應(yīng)用程序的什么地方,并且注意在單獨(dú)線程上進(jìn)行不直接涉及 UI 的所有處理。在大多數(shù)情況下,可以使用 ThreadPool 類創(chuàng)建線程。然而,在某些情況下,必須使用 Thread 類來作為代替,在另外一些情況下,需要使用委托對象或 Web 服務(wù)代理來使特定的處理在非 UI 線程上進(jìn)行。
在多線程應(yīng)用程序中,必須確保 UI 線程負(fù)責(zé)所有與 UI 有關(guān)的任務(wù),這樣就可以有效地管理 UI 線程和其他線程之間的通信。Task 模式可以幫助大大簡化這種交互。