C# 執行緒(一)

weixin_34402090發表於2014-08-13

From : http://www.cnblogs.com/miniwiki/archive/2010/06/18/1760540.html

文章系參考轉載,英文原文網址請參考:http://www.albahari.com/threading/

作者 Joseph Albahari,  翻譯 Swanky Wu

  中文翻譯作者把原文放在了"google 協作"上面,GFW遮蔽,不能訪問和檢視,因此我根據譯文和英文原版整理轉載到園子裡面。

  本系列文章可以算是一本很出色的C#執行緒手冊,思路清晰,要點都有介紹,看了後對C#的執行緒及同步等有了更深入的理解。

  • 入門
  • 執行緒同步基礎
    • 同步要領
    • 鎖和執行緒安全
    • Interrupt 和 Abort
    • 執行緒狀態
    • 等待控制程式碼
    • 同步環境
  • 使用多執行緒
    • 單元模式和Windows Forms
    • BackgroundWorker類
    • ReaderWriterLock類
    • 執行緒池
    • 非同步委託
    • 計時器
    • 區域性儲存
  • 高階話題
    • 非阻止同步
    • Wait和Pulse
    • Suspend和Resume
    • 終止執行緒

一、入門

1.     概述與概念

   C#支援通過多執行緒並行地執行程式碼,一個執行緒有它獨立的執行路徑,能夠與其它的執行緒同時地執行。一個C#程式開始於一個單執行緒,這個單執行緒是被CLR和作業系統(也稱為“主執行緒”)自動建立的,並具有多執行緒建立額外的執行緒。這裡的一個簡單的例子及其輸出:

     除非被指定,否則所有的例子都假定以下名稱空間被引用了:  
   using System; 
   using System.Threading;

1
2
3
4
5
6
7
8
9
10
11
class ThreadTest {
  static void Main() {
    Thread t = new Thread (WriteY);
    t.Start();                          // Run WriteY on the new thread
    while (true) Console.Write ("x");   // Write 'x' forever
  }
  
  static void WriteY() {
    while (true) Console.Write ("y");   // Write 'y' forever
  }
}

image

   主執行緒建立了一個新執行緒“t”,它執行了一個重複列印字母"y"的方法,同時主執行緒重複但因字母“x”。CLR分配每個執行緒到它自己的記憶體堆疊上,來保證區域性變數的分離執行。在接下來的方法中我們定義了一個區域性變數,然後在主執行緒和新建立的執行緒上同時地呼叫這個方法。

1
2
3
4
5
6
7
8
9
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 ('?');
}

image

   變數cycles的副本分別在各自的記憶體堆疊中建立,輸出也一樣,可預見,會有10個問號輸出。當執行緒們引用了一些公用的目標例項的時候,他們會共享資料。下面是例項:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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"); }
 }
}
因為在相同的<b>ThreadTest</b>例項中,兩個執行緒都呼叫了<b>Go()</b>,它們共享了<b>done</b>欄位,這個結果輸出的是一個"Done",而不是兩個。
1
<a href="http://images.cnblogs.com/cnblogs_com/miniwiki/WindowsLiveWriter/C_12936/image_6.png"><img height="45" width="640" src="http://images.cnblogs.com/cnblogs_com/miniwiki/WindowsLiveWriter/C_12936/image_thumb_2.png" align="left" alt="image" border="0" title="image" style="display: inline; margin-left: 0px; margin-right: 0px; border-width: 0px;"></a>

 

  靜態欄位提供了另一種線上程間共享資料的方式,下面是一個以done為靜態欄位的例子:

1
2
3
4
5
6
7
8
9
10
11
12
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"被列印兩次的機會會大幅地上升:

1
2
3
static void Go() {
  if (!done) { Console.WriteLine ("Done"); done = true; }
}

image

問題就是一個執行緒在判斷if塊的時候,正好另一個執行緒正在執行WriteLine語句——在它將done設定為true之前。

補救措施是當讀寫公共欄位的時候,提供一個排他鎖;C#提供了lock語句來達到這個目的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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一段時間:

1
Thread.Sleep (TimeSpan.FromSeconds (30));         // Block for 30 seconds

一個執行緒也可以使用它的Join方法來等待另一個執行緒結束:

1
2
3
Thread t = new Thread (Go);           // Assume Go is some static method
t.Start();
t.Join();                             // Wait (block) until thread t ends

一個執行緒,一旦被阻止,它就不再消耗CPU的資源了。

  執行緒是如何工作的

   執行緒被一個執行緒協調程式管理著——一個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操作是非常棘手的,當只有一個或兩個工作執行緒要比有眾多的執行緒在相同時間執行任務塊的多。稍後我們將實現生產者/耗費者 佇列,它提供了上述功能。

 

2.    建立和開始使用多執行緒

   執行緒用Thread類來建立, 通過ThreadStart委託來指明方法從哪裡開始執行,下面是ThreadStart委託如何定義的:

1
public delegate void ThreadStart();

   呼叫Start方法後,執行緒開始執行,執行緒一直到它所呼叫的方法返回後結束。下面是一個例子,使用了C#的語法建立TheadStart委託:

1
2
3
4
5
6
7
class ThreadTest {
  static void Main() {
    Thread t = new Thread (new ThreadStart (Go));
    t.Start();   // Run Go() on the new thread.
    Go();        // Simultaneously run Go() in the main thread.
  }
  static void Go() { Console.WriteLine ("hello!"); }

在這個例子中,執行緒t執行Go()方法,大約與此同時主執行緒也呼叫了Go(),結果是兩個幾乎同時hello被列印出來:

image

一個執行緒可以通過C#堆委託簡短的語法更便利地建立出來:

1
2
3
4
5
6
7
static void Main() {
  Thread t = new Thread (Go);    // No need to explicitly use ThreadStart
  t.Start();
  ...
}
static void Go() { ... }
在這種情況,ThreadStart被編譯器自動推斷出來,另一個快捷的方式是使用匿名方法來啟動執行緒:
1
2
3
4
static void Main() {
  Thread t = new Thread (delegate() { Console.WriteLine ("Hello!"); });
  t.Start();
}

  執行緒有一個IsAlive屬性,在呼叫Start()之後直到執行緒結束之前一直為true。一個執行緒一旦結束便不能重新開始了。

  將資料傳入ThreadStart中

  話又說回來,在上面的例子裡,我們想更好地區分開每個執行緒的輸出結果,讓其中一個執行緒輸出大寫字母。我們傳入一個狀態字到Go中來完成整個任務,但我們不能使用ThreadStart委託,因為它不接受引數,所幸的是,.NET framework定義了另一個版本的委託叫做ParameterizedThreadStart, 它可以接收一個單獨的object型別引數:

1
2
public delegate void ParameterizedThreadStart (object obj);
之前的例子看起來是這樣的:
1
  
1
2
3
4
5
6
7
8
9
10
class ThreadTest {
  static void Main() {
    Thread t = new Thread (Go);
    t.Start (true);             // == Go (true)
    Go (false);
  }
  static void Go (object upperCase) {
    bool upper = (bool) upperCase;
    Console.WriteLine (upper ? "HELLO!" : "hello!");
  }

image

  在整個例子中,編譯器自動推斷出ParameterizedThreadStart委託,因為Go方法接收一個單獨的object引數,就像這樣寫:

1
2
Thread t = new Thread (new ParameterizedThreadStart (Go));
t.Start (true);

ParameterizedThreadStart的特性是在使用之前我們必需對我們想要的型別(這裡是bool)進行裝箱操作,並且它只能接收一個引數。

  一個替代方案是使用一個匿名方法呼叫一個普通的方法如下:

1
2
3
4
5
static void Main() {
  Thread t = new Thread (delegate() { WriteText ("Hello"); });
  t.Start();
}
static void WriteText (string text) { Console.WriteLine (text); }

  優點是目標方法(這裡是WriteText),可以接收任意數量的引數,並且沒有裝箱操作。不過這需要將一個外部變數放入到匿名方法中,向下面的一樣:

1
2
3
4
5
6
7
static void Main() {
  string text = "Before";
  Thread t = new Thread (delegate() { WriteText (text); });
  text = "After";
  t.Start();
}
static void WriteText (string text) { Console.WriteLine (text); }

image

  匿名方法開啟了一種怪異的現象,當外部變數被後來的部分修改了值的時候,可能會透過外部變數進行無意的互動。有意的互動(通常通過欄位)被認為是足夠了!一旦執行緒開始執行了,外部變數最好被處理成只讀的——除非有人願意使用適當的鎖。

  另一種較常見的方式是將物件例項的方法而不是靜態方法傳入到執行緒中,物件例項的屬性可以告訴執行緒要做什麼,如下列重寫了原來的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
class ThreadTest {
  bool upper;
  
  static void Main() {
    ThreadTest instance1 = new ThreadTest();
    instance1.upper = true;
    Thread t = new Thread (instance1.Go);
    t.Start();
    ThreadTest instance2 = new ThreadTest();
    instance2.Go();        // 主執行緒——執行 upper=false
  }
  
  void Go() { Console.WriteLine (upper ? "HELLO!" : "hello!"); }

  命名執行緒

  執行緒可以通過它的Name屬性進行命名,這非產有利於除錯:可以用Console.WriteLine列印出執行緒的名字,Microsoft Visual Studio可以將執行緒的名字顯示在除錯工具欄的位置上。執行緒的名字可以在被任何時間設定——但只能設定一次,重新命名會引發異常。

  程式的主執行緒也可以被命名,下面例子裡主執行緒通過CurrentThread命名:

1
2
3
4
5
6
7
8
9
10
11
12
class ThreadNaming {
  static void Main() {
    Thread.CurrentThread.Name = "main";
    Thread worker = new Thread (Go);
    worker.Name = "worker";
    worker.Start();
    Go();
  }
  static void Go() {
    Console.WriteLine ("Hello from " + Thread.CurrentThread.Name);
  }
}

image

  

  前臺和後臺執行緒

  執行緒預設為前臺執行緒,這意味著任何前臺執行緒在執行都會保持程式存活。C#也支援後臺執行緒,當所有前臺執行緒結束後,它們不維持程式的存活。

  改變執行緒從前臺到後臺不會以任何方式改變它在CPU協調程式中的優先順序和狀態。

  執行緒的IsBackground屬性控制它的前後臺狀態,如下例項:

1
2
3
4
5
6
7
class PriorityTest {
  static void Main (string[] args) {
    Thread worker = new Thread (delegate() { Console.ReadLine(); });
    if (args.Length > 0) worker.IsBackground = true;
    worker.Start();
  }
}

   如果程式被呼叫的時候沒有任何引數,工作執行緒為前臺執行緒,並且將等待ReadLine語句來等待使用者的觸發回車,這期間,主執行緒退出,但是程式保持執行,因為一個前臺執行緒仍然活著。

   另一方面如果有引數傳入Main(),工作執行緒被賦值為後臺執行緒,當主執行緒結束程式立刻退出,終止了ReadLine。

   後臺執行緒終止的這種方式,使任何最後操作都被規避了,這種方式是不太合適的。好的方式是明確等待任何後臺工作執行緒完成後再結束程式,可能用一個timeout(大多用Thread.Join)。如果因為某種原因某個工作執行緒無法完成,可以用試圖終止它的方式,如果失敗了,再拋棄執行緒,允許它與 與程式一起消亡。(記錄是一個難題,但這個場景下是有意義的)

   擁有一個後臺工作執行緒是有益的,最直接的理由是它當提到結束程式它總是可能有最後的發言權。交織以不會消亡的前臺執行緒,保證程式的正常退出。拋棄一個前臺工作執行緒是尤為險惡的,尤其對Windows Forms程式,因為程式直到主執行緒結束時才退出(至少對使用者來說),但是它的程式仍然執行著。在Windows工作管理員它將從應用程式欄消失不見,但卻可以在程式欄找到它。除非使用者找到並結束它,它將繼續消耗資源,並可能阻止一個新的例項的執行從開始或影響它的特性。

   對於程式失敗退出的普遍原因就是存在“被忘記”的前臺執行緒。

 

  執行緒優先順序

  執行緒的Priority 屬性確定了執行緒相對於其它同一程式的活動的執行緒擁有多少執行時間,以下是級別:

1
enum ThreadPriority { Lowest, BelowNormal, Normal, AboveNormal, Highest }

  只有多個執行緒同時為活動時,優先順序才有作用。

  設定一個執行緒的優先順序為高一些,並不意味著它能執行實時的工作,因為它受限於程式的程式的級別。要執行實時的工作,必須提升在System.Diagnostics 名稱空間下Process的級別,像下面這樣:(我沒有告訴你如何做到這一點:))

1
Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.High;

   ProcessPriorityClass.High 其實是一個短暫缺口的過程中的最高優先順序別:Realtime。設定程式級別到Realtime通知作業系統:你不想讓你的程式被搶佔了。如果你的程式進入一個偶然的死迴圈,可以預期,作業系統被鎖住了,除了關機沒有什麼可以拯救你了!基於此,High大體上被認為最高的有用程式級別。

   如果一個實時的程式有一個使用者介面,提升程式的級別是不太好的,因為當使用者介面UI過於複雜的時候,介面的更新耗費過多的CPU時間,拖慢了整臺電腦。(雖然在寫這篇文章的時候,在網際網路電話程式Skype僥倖地這麼做, 也許是因為它的介面相當簡單吧。) 降低主執行緒的級別、提升程式的級別、確保實時執行緒不進行介面重新整理,但這樣並不能避免電腦越來越慢,因為作業系統仍會撥出過多的CPU給整個程式。最理想的方案是使實時工作和使用者介面在不同的程式(擁有不同的優先順序)執行,通過Remoting或共享記憶體方式進行通訊,共享記憶體需要Win32 API中的 P/Invoking。(可以搜尋看看CreateFileMapping  MapViewOfFile)

  

  異常處理

  任何執行緒建立範圍內try/catch/finally塊,當執行緒開始執行便不再與其有任何關係。考慮下面的程式:

1
2
3
4
5
6
7
8
9
10
11
public static void Main() {
 try {
   new Thread (Go).Start();
 }
 catch (Exception ex) {
   // 不會在這得到異常
   Console.WriteLine ("Exception!");
 }
 
 static void Go() { throw null; }
}
1
這裡try/catch語句一點用也沒有,新建立的執行緒將引發NullReferenceException異常。當你考慮到每個執行緒有獨立的執行路徑的時候,便知道這行為是有道理的,
1
補救方法是線上程處理的方法內加入他們自己的異常處理:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void Main() {
   new Thread (Go).Start();
}
  
static void Go() {
  try {
    ...
    throw null;      // 這個異常在下面會被捕捉到
    ...
  }
  catch (Exception ex) {
    記錄異常日誌,並且或通知另一個執行緒
    我們發生錯誤
    ...
  }

   從.NET 2.0開始,任何執行緒內的未處理的異常都將導致整個程式關閉,這意味著忽略異常不再是一個選項了。因此為了避免由未處理異常引起的程式崩潰,try/catch塊需要出現在每個執行緒進入的方法內,至少要在產品程式中應該如此。對於經常使用“全域性”異常處理的Windows Forms程式設計師來說,這可能有點麻煩,像下面這樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
using System;
using System.Threading;
using System.Windows.Forms;
  
static class Program {
  static void Main() {
    Application.ThreadException += HandleError;
    Application.Run (new MainForm());
  }
  
  static void HandleError (object sender, ThreadExceptionEventArgs e) {
    記錄異常或者退出程式或者繼續執行...
  }
}

 

Application.ThreadException事件在異常被丟擲時觸發,以一個Windows資訊(比如:鍵盤,滑鼠活著 "paint" 等資訊)的方式,簡言之,一個Windows Forms程式的幾乎所有程式碼。雖然這看起來很完美,它使人產生一種虛假的安全感——所有的異常都被中央異常處理捕捉到了。由工作執行緒丟擲的異常便是一個沒有被Application.ThreadException捕捉到的很好的例外。(在Main方法中的程式碼,包括構造器的形式,在Windows資訊開始前先執行)

.NET framework為全域性異常處理提供了一個更低階別的事件:AppDomain.UnhandledException,這個事件在任何型別的程式(有或沒有使用者介面)的任何執行緒有任何未處理的異常觸發。儘管它提供了好的不得已的異常處理解決機制,但是這不意味著這能保證程式不崩潰,也不意味著能取消.NET異常對話方塊。

在產品程式中,明確地使用異常處理在所有執行緒進入的方法中是必要的,可以使用包裝類和幫助類來分解工作來完成任務,比如使用BackgroundWorker類(在第三部分進行討論)

 

 

 

-

-------------------->>>

 

相關文章