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