C#中的線程(一)入門

Keywords:C# 線程
Source:http://www.albahari.com/threading/
Author: Joe Albahari
Translator: Swanky Wu
Published: http://www.cnblogs.com/txw1958/
Download:http://www.albahari.info/threading/threading.pdf html

 

第一部分:入門

 

概述與概念

C#支持經過多線程並行地執行代碼,一個線程有它獨立的執行路徑,可以與其它的線程同時地運行。一個C#程序開始於一個單線程,這個單線程是被CLR和操做系統(也稱爲「主線程」)自動建立的,並具備多線程建立額外的線程。這裏的一個簡單的例子及其輸出:程序員

除非被指定,不然全部的例子都假定如下命名空間被引用了:
using System;
using System.Threading;web

class ThreadTest {
  static void Main() {
    Thread t = new Thread (WriteY);
    t.Start();                          // 在新的線程中運行WriteY
    while (true) Console.Write ("x");   // 不停地寫'x'
  }
 
  static void WriteY() {
    while (true) Console.Write ("y");   // 不停地寫'y'
  }
}

xxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxyyyyyyyyyyyyy
yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
yyyyyyyyyyyyyxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
...數據庫

主線程建立了一個新線程「t」,它運行了一個重複打印字母"y"的方法,同時主線程重複但因字母「x」。CLR分配每一個線程到它本身的內存堆棧上,來保證局部變量的分離運行。在接下來的方法中咱們定義了一個局部變量,而後在主線程和新建立的線程上同時地調用這個方法。安全

static void Main() {
  new Thread (Go).Start();      // 調用Go()方法在一個新線程中
  Go();                         // 在主線程中調用Go()
}
 
static void Go() {
// 聲明和使用一個局部變量'cycles'
  for (int cycles = 0; cycles < 5; cycles++) Console.Write ('?');
}

??????????服務器

變量cycles的副本分別在各自的內存堆棧中建立,輸出也同樣,可預見,會有10個問號輸出。當線程們引用了一些公用的目標實例的時候,他們會共享數據。下面是實例:多線程

class ThreadTest {
 bool done;
 
 static void Main() {
   ThreadTest tt = new ThreadTest();   // 建立一個實例
   new Thread (tt.Go).Start();
   tt.Go();
 }
 
// 注意Go如今是一個實例方法
 void Go() {
   if (!done) { done = true; Console.WriteLine ("Done"); }
 }
}

由於在相同的ThreadTest實例中,兩個線程都調用了Go(),它們共享了done字段,這個結果輸出的是一個"Done",而不是兩個。併發

Doneapp

靜態字段提供了另外一種在線程間共享數據的方式,下面是一個以done爲靜態字段的例子:函數

class ThreadTest {
 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; }
}

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));         // 阻止30秒

一個線程也可使用它的Join方法來等待另外一個線程結束:

Thread t = new Thread (Go);           // 假設Go是某個靜態方法
t.Start();
t.Join();                             // 等待(阻止)直到線程t結束

一個線程,一旦被阻止,它就再也不消耗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操做是很是棘手的,當只有一個或兩個工做線程要比有衆多的線程在相同時間執行任務塊的多。稍後咱們將實現生產者/耗費者 隊列,它提供了上述功能。

建立和開始使用多線程

線程用Thread類來建立, 經過ThreadStart委託來指明方法從哪裏開始運行,下面是ThreadStart委託如何定義的:

public delegate void ThreadStart();

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

class ThreadTest {
  static void Main() {
    Thread t = new Thread (new ThreadStart (Go));
    t.Start();   // 在新線程中運行Go()
    Go();        // 同時在主線程中運行Go() 
  }
  static void Go() { Console.WriteLine ("hello!"); }

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

hello!
hello!

一個線程能夠經過C#堆委託簡短的語法更便利地建立出來:

static void Main() {
  Thread t = new Thread (Go);    // 不必明確地使用ThreadStart
  t.Start();
  ...
}
static void Go() { ... }

在這種狀況,ThreadStart被編譯器自動推斷出來,另外一個快捷的方式是使用匿名方法來啓動線程:

static void Main() {
  Thread t = new Thread (delegate() { Console.WriteLine ("Hello!"); });
  t.Start();
}

線程有一個IsAlive屬性,在調用Start()以後直到線程結束以前一直爲true。

一個線程一旦結束便不能從新開始了。

將數據傳入ThreadStart中

話又說回來,在上面的例子裏,咱們想更好地區分開每一個線程的輸出結果,讓其中一個線程輸出大寫字母。咱們傳入一個狀態字到Go中來完成整個任務,但咱們不能使用ThreadStart委託,由於它不接受參數,所幸的是,.NET framework定義了另外一個版本的委託叫作ParameterizedThreadStart, 它能夠接收一個單獨的object類型參數:

public delegate void ParameterizedThreadStart (object obj);

以前的例子看起來是這樣的:

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!");
  }

hello!
HELLO!

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

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

ParameterizedThreadStart的特性是在使用以前咱們必需對咱們想要的類型(這裏是bool)進行裝箱操做,而且它只能接收一個參數。

一個替代方案是使用一個匿名方法調用一個普通的方法以下:

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

優勢是目標方法(這裏是WriteText),能夠接收任意數量的參數,而且沒有裝箱操做。不過這須要將一個外部變量放入到匿名方法中,向下面的同樣:

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); }

After

 

匿名方法打開了一種怪異的現象,當外部變量被後來的部分修改了值的時候,可能會透過外部變量進行無心的互動。有意的互動(一般經過字段)被認爲是足夠了!一旦線程開始運行了,外部變量最好被處理成只讀的——除非有人願意使用適當的鎖。

另外一種較常見的方式是將對象實例的方法而不是靜態方法傳入到線程中,對象實例的屬性能夠告訴線程要作什麼,以下列重寫了原來的例子:

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命名:

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);
  }
}

Hello from main
Hello from worker

前臺和後臺線程

線程默認爲前臺線程,這意味着任何前臺線程在運行都會保持程序存活。C#也支持後臺線程,當全部前臺線程結束後,它們不維持程序的存活。

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

線程的IsBackground屬性控制它的先後臺狀態,以下實例:

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 屬性肯定了線程相對於其它同一進程的活動的線程擁有多少執行時間,如下是級別:

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

只有多個線程同時爲活動時,優先級纔有做用。

設置一個線程的優先級爲高一些,並不意味着它能執行實時的工做,由於它受限於程序的進程的級別。要執行實時的工做,必須提高在System.Diagnostics 命名空間下Process的級別,像下面這樣:(我沒有告訴你如何作到這一點 )

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

ProcessPriorityClass.High 實際上是一個短暫缺口的過程當中的最高優先級別:Realtime。設置進程級別到Realtime通知操做系統:你不想讓你的進程被搶佔了。若是你的程序進入一個偶然的死循環,能夠預期,操做系統被鎖住了,除了關機沒有什麼能夠拯救你了!基於此,High大致上被認爲最高的有用進程級別。

若是一個實時的程序有一個用戶界面,提高進程的級別是不太好的,由於當用戶界面UI過於複雜的時候,界面的更新耗費過多的CPU時間,拖慢了整臺電腦。(雖然在寫這篇文章的時候,在互聯網電話程序Skype僥倖地這麼作, 也許是由於它的界面至關簡單吧。) 下降主線程的級別、提高進程的級別、確保實時線程不進行界面刷新,但這樣並不能避免電腦愈來愈慢,由於操做系統仍會撥出過多的CPU給整個進程。最理想的方案是使實時工做和用戶界面在不一樣的進程(擁有不一樣的優先級)運行,經過Remoting或共享內存方式進行通訊,共享內存須要Win32 API中的 P/Invoking。(能夠搜索看看CreateFileMapping  MapViewOfFile)

異常處理

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

public static void Main() {
  try {
    new Thread (Go).Start();
  }
  catch (Exception ex) {
    // 不會在這獲得異常
    Console.WriteLine ("Exception!");
  }
 
  static void Go() { throw null; }
}

這裏try/catch語句一點用也沒有,新建立的線程將引起NullReferenceException異常。當你考慮到每一個線程有獨立的執行路徑的時候,便知道這行爲是有道理的,補救方法是在線程處理的方法內加入他們本身的異常處理:

public static void Main() {
   new Thread (Go).Start();
}
 
static void Go() {
  try {
    ...
    throw null;      // 這個異常在下面會被捕捉到
    ...
  }
  catch (Exception ex) {
    記錄異常日誌,而且或通知另外一個線程
    咱們發生錯誤
    ...
  }

從.NET 2.0開始,任何線程內的未處理的異常都將致使整個程序關閉,這意味着忽略異常再也不是一個選項了。所以爲了不由未處理異常引發的程序崩潰,try/catch塊須要出如今每一個線程進入的方法內,至少要在產品程序中應該如此。對於常用「全局」異常處理的Windows Forms程序員來講,這可能有點麻煩,像下面這樣:

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類(在第三部分進行討論)。

相關文章
相關標籤/搜索