前言
C#支援通過多線程並行地執行代碼,一個線程有它獨立的執行路徑,能夠與其它的線程同時地運行。一個C#程式開始於一個單線程,這個單線程是被CLR和作業系統(也稱為“主線程”)自動建立的,並具有多線程建立額外的線程。
簡單樣本說明
這裡的一個簡單的例子及其輸出
static void Main(string[] args) { System.Threading.Thread thread = new System.Threading.Thread(WriteY); thread.Start(); while (true) { Console.Write("X"); } } static void WriteY() { while (true) { Console.Write("Y"); } }
當然還要引用命名空間using System.Threading;
F5執行以後
主線程建立了一個新線程“t”,它運行了一個重複列印字母"y"的方法,同時主線程重複但因字母“x”。CLR分配每個線程到它自己的記憶體堆棧上,來保證局部變數的分離運行。在接下來的方法中我們定義了一個局部變數,然後在主線程和新建立的線程上同時地調用這個方法。
再來看一個小例子
static void Main(string[] args) { new System.Threading.Thread(Go).Start(); Go(); Console.ReadLine(); } static void Go() { for (int cycles = 0; cycles < 5; cycles++) Console.Write('?'); }
變數cycles的副本分別在各自的記憶體堆棧中建立,輸出也一樣,可預見,會有10個問號輸出。當線程們引用了一些公用的目標執行個體的時候,他們會共用資料。下面是執行個體:
class Program { bool done; static void Main(string[] args) { Program tt = new Program(); // Create a common instance new System.Threading.Thread(tt.Go).Start(); tt.Go(); Console.ReadLine(); } void Go() { if (!done) { done = true; Console.WriteLine("Done"); } } }
因為在相同的Program執行個體中,兩個線程都調用了Go(),它們共用了done欄位,這個結果輸出的是一個"Done",而不是兩個。
靜態欄位提供了另一種線上程間共用資料的方式,下面是一個以done為靜態欄位的例子:
static bool done; static void Main(string[] args) { new System.Threading.Thread(Go).Start(); Go(); Console.ReadLine(); } 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之前。
補救措施是當讀寫公用欄位的時候,提供一個獨佔鎖定;C#提供了lock語句來達到這個目的:
class Program { static bool done; static object locker = new object(); static void Main(string[] args) { new System.Threading.Thread(Go).Start(); Go(); Console.ReadLine(); } 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();
總結
線程是如何工作的
線程被一個線程協調程式管理著——一個CLR委託給作業系統的函數。線程協調程式確保將所有活動的線程被分配適當的執行時間;並且那些等待或阻止的線程——比如說在排它鎖中、或在使用者輸入——都是不消耗CPU時間的。
在單核處理器的電腦中,線程協調程式完成一個時間片之後迅速地在活動的線程之間進行切換執行。這就導致“波濤洶湧”的行為,例如在第一個例子,每次重複的X 或 Y 塊相當於分給線程的時間片。在Windows XP中時間片通常在10毫秒內選擇要比CPU開銷在處理線程切換的時候的消耗大的多。(即通常在幾微秒區間)
在多核的電腦中,多線程被實現成混合時間片和真實的並發——不同的線程在不同的CPU上運行。這幾乎可以肯定仍然會出現一些時間切片, 由於作業系統的需要服務自己的線程,以及一些其他的應用程式。
線程由於外部因素(比如時間片)被中斷被稱為被搶佔,在大多數情況下,一個線程方面在被搶佔的那一時那一刻就失去了對它的控制權。
線程 vs. 進程
屬於一個單一的應用程式的所有的線程邏輯上被包含在一個進程中,進程指一個應用程式所啟動並執行作業系統單元。
線程於進程有某些相似的地方:比如說進程通常以時間片方式與其它在電腦中啟動並執行進程的方式與一個C#程式線程啟動並執行方式大致相同。二者的關鍵區別在於進程彼此是完全隔絕的。線程與運行在相同程式其它線程共用(堆heap)記憶體,這就是線程為何如此有用:一個線程可以在後台讀取資料,而另一個線程可以在前台展現已讀取的資料。
何時使用多線程
多線程程式一般被用來在後台執行耗時的任務。主線程保持運行,並且背景工作執行緒做它的後台工作。對於Windows Forms程式來說,如果主線程試圖執行冗長的操作,鍵盤和滑鼠的操作會變的遲鈍,程式也會失去響應。由於這個原因,應該在背景工作執行緒中運行一個耗時任務時添加一個背景工作執行緒,即使在主線程上有一個有好的提示“處理中...”,以防止工作無法繼續。這就避免了程式出現由作業系統提示的“沒有相應”,來誘使使用者強制結束程式的進程而導致錯誤。強制回應對話方塊還允許實現“取消”功能,允許繼續接收事件,而實際的任務已被背景工作執行緒完成。BackgroundWorker恰好可以輔助完成這一功能。
在沒有使用者介面的程式裡,比如說Windows Service, 多線程在當一個任務有潛在的耗時,因為它在等待另台電腦的響應(比如一個應用伺服器,資料庫伺服器,或者一個用戶端)的實現特別有意義。用背景工作執行緒完成任務意味著主線程可以立即做其它的事情。
另一個多線程的用途是在方法中完成一個複雜的計算工作。這個方法會在多核的電腦上啟動並執行更快,如果工作量被多個線程分開的話(使用Environment.ProcessorCount屬性來偵測處理晶片的數量)。
一個C#程式稱為多線程的可以通過2種方式:明確地建立和運行多線程,或者使用.NET framework的暗中使用了多線程的特性——比如BackgroundWorker類, 線程池,threading timer,遠程伺服器,或Web Services或ASP.NET程式。在後面的情況,人們別無選擇,必須使用多線程;一個單線程的ASP.NET web server不是太酷,即使有這樣的事情;幸運的是,應用伺服器中多線程是相當普遍的;唯一值得關心的是提供適當鎖機制的靜態變數問題。
何時不要使用多線程
多線程也同樣會帶來缺點,最大的問題是它使程式變的過於複雜,擁有多線程本身並不複雜,複雜是的線程的互動作用,這帶來了無論是否互動是否是有意的,都會帶來較長的開發週期,以及帶來間歇性和非重複性的bugs。因此,要麼多線程的互動設計簡單一些,要麼就根本不使用多線程。除非你有強烈的重寫和調試慾望。
當使用者頻繁地分配和切換線程時,多線程會帶來增加資源和CPU的開銷。在某些情況下,太多的I/O操作是非常棘手的,當只有一個或兩個背景工作執行緒要比有眾多的線程在相同時間執行任務塊的多。稍後我們將實現生產者/耗費者 隊列,它提供了上述功能。