C#支持通過多線程並行地執行代碼,一個線程有它獨立的執行路徑,能夠與其它的線程同時地運行。一個C#程序開始於一個單線程,這個單線程是被CLR和操作系統(也稱為“主線程”)自動創建的。
一個簡單示例如下:
using System; using System.Threading; class ThreadDemo { static void Main() { Thread t = new Thread (WriteY); t.Start(); while (true) Console.Write ("x"); } static void WriteY() { while (true) Console.Write ("y"); } }
主線程創建了一個新線程“t”,運行了一個重復打印字母"y"的方法,同時主線程重復打印字母“x”。CLR分配每個線程到它自己的內存堆棧上,來保證局部變量的分離運行。在接下來的方法中我們定義了一個局部變量,然後在主線程和新創建的線程上同時地調用這個方法。
static void Main() { new Thread (Go).Start(); Go(); } static void Go() { for (int cycles = 0; cycles < 5; cycles++) Console.Write ('?'); }
變量cycles的副本分別在各自的內存堆棧中創建,因此會有10個問號輸出。而當線程們引用了一些公用的目標實例的時候,他們會共享數據。示例如下:
class ThreadDemo { bool done; static void Main() { ThreadDemo tt = new ThreadDemo(); new Thread (tt.Go).Start(); tt.Go(); } void Go() { if (!done) { done = true; Console.WriteLine ("Done"); } } }
在上面的示例中,兩個線程都調用了Go方法,它們共享了done字段,這個結果輸出的是一個"Done",而不是兩個。
靜態字段則提供了另一種在線程間共享數據的方式,下面是一個以done為靜態字段的例子:
class ThreadDemo { static bool done; // 靜態字段在所有線程中共享 static void Main() { new Thread (Go).Start(); Go(); } static void Go() { if (!done) { done = true; Console.WriteLine ("Done"); } } }
上述兩個示例足以說明另一個關鍵概念, 即線程安全, 輸出實際上是不確定的:可能輸出一次done,也可能打印兩次。如果我們在Go方法裡語句順序, "Done"被打印兩次的機會會大幅地上升,如:
static void Go() { if (!done) { Console.WriteLine ("Done"); done = true; } }
問題出在當一個線程正在判斷if塊的時候,正好另一個線程在執行WriteLine語句,而且在它將done設置為true之前,導致輸出兩次done。補救措施是對讀寫公共字段提供一個排他鎖;C#提供了lock語句來達到這個目的:
class ThreadSafe { static bool done; static object locker = new object(); static void Main() { new Thread (Go).Start(); Go(); } static void Go() { lock (locker) { if (!done) { Console.WriteLine ("Done"); done = true; } } } }
當兩個線程爭奪一個鎖的時候(在這個例子裡是locker),一個線程等待,或者說被阻止到那個鎖變的可用。在這種情況下,就確保了在同一時刻只有一個線程能進入臨界區,所以"Done"只被打印了1次。這樣的代碼編寫方式在不確定的多線程環境中被叫做線程安全。
臨時暫停或阻止是多線程同步活動的本質特征。等待一個排它鎖被釋放是一個線程被阻止的原因,另一個原因是線程想要暫停或Sleep一段時間:
Thread.Sleep (TimeSpan.FromSeconds (30));
一個線程也可以使用它的Join方法來等待另一個線程結束:
Thread t = new Thread (Go); t.Start(); t.Join();
一個線程,一旦被阻止,它就不再消耗CPU的資源了。
線程是如何工作的
線程在.NET中由一個線程協調程序管理著——一個CLR委托給操作系統的函數。線程協調程序確保將所有活動的線程被分配適當的執行時間;並且那些等待或阻止的線程——比如說在排它鎖中、或在用戶輸入——都是不消耗CPU時間的。
在單核處理器電腦中,線程協調程序完成一個時間片之後迅速地在活動的線程之間進行切換執行。這就導致“波濤洶湧”的行為,例如在第一個例子,每次重復的X 或 Y 塊相當於分給線程的時間片。在Windows XP中時間片通常在10毫秒內選擇要比CPU開銷在處理線程切換的時候的消耗大的多,即通常在幾微秒區間。在多核電腦中,多線程被實現成混合時間片和真實的並發——不同的線程在不同的CPU上運行。這幾乎可以肯定仍然會出現一些時間切片, 由於操作系統的需要服務自己的線程,以及一些其他的應用程序。
線程由於外部因素(比如時間片)被中斷被稱為被搶占,在大多數情況下,一個線程方面在被搶占的那一時那一刻就失去了對它的控制權。
線程 vs. 進程
屬於一個單一應用程序的所有的線程邏輯上被包含在一個進程中,進程指一個應用程序所運行的操作系統單元。
線程於進程有某些相似的地方,進程在電腦中運行方式與一個C#程序線程運行的方式大致相同。二者的關鍵區別在於進程彼此是完全隔絕的。線程與運行在相同程序其它線程共享堆內存,這就是線程為何如此有用:一個線程可以在後台讀取數據,而另一個線程可以在前台展現已讀取的數據。
何時使用多線程
多線程程序一般被用來在後台執行耗時的任務。主線程保持運行,並且工作線程做它的後台工作。對於Windows Forms程序來說,如果主線程試圖執行冗長的操作,鍵盤和鼠標的操作會變的遲鈍,程序也會失去響應。由於這個原因,應該在工作線程中運行一個耗時任務時添加一個工作線程,即使在主線程上有一個有好的提示“處理中...”,以防止工作無法繼續。這就避免了程序出現由操作系統提示的“沒有響應”,使得用戶強制結束程序的進程而導致錯誤。模式對話框還允許實現“取消”功能,允許繼續接收事件,而實際的任務已被工作線程完成。BackgroundWorker恰好可以輔助完成這一功能。
在沒有用戶界面的程序裡,比如說Windows Service, 多線程在當一個任務有潛在的耗時,因為它在等待另台電腦的響應(比如一個應用服務器,數據庫服務器,或者一個客戶端)的實現特別有意義。用工作線程完成任務意味著主線程可以立即做其它的事情。
另一個多線程的用途是在方法中完成一個復雜的計算工作。這個方法會在多核的電腦上運行的更快,如果工作量被多個線程分開的話(使用Environment.ProcessorCount屬性來偵測處理芯片的數量)。
在C#程序可以通過以下2種方式使用多線程:明確地創建和運行多線程,或者使用.NET framework的暗中使用了多線程的特性——比如BackgroundWorker類, 線程池,threading timer,遠程服務器,或Web Services或ASP.NET程序。在應用服務器中多線程是相當普遍的;唯一值得關心的是提供適當鎖機制的靜態變量問題。
何時不要使用多線程
多線程也同樣會帶來缺點,最大的問題是它使程序變的過於復雜,擁有多線程本身並不復雜,復雜是的線程的交互作用,無論這種交互是否有意,都會帶來較長的開發周期,間歇性和非重復性的bugs。因此,要麼多線程的交互設計簡單一些,要麼就根本不使用多線程,除非你有強烈的重寫和調試欲望。
當用戶頻繁地分配和切換線程時,多線程會帶來增加資源和CPU的開銷。在某些情況下,太多的I/O操作是非常棘手的,當只有一個或兩個工作線程要比有眾多的線程在相同時間執行任務塊的多。稍後我們將實現生產者/耗費者 隊列,它提供了上述功能。