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