【轉】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#的線程及同步等有了更深刻的理解。web

  • 入門
    • 概述與概念
    • 建立和開始使用多線程
  • 線程同步基礎
    • 同步要領
    • 鎖和線程安全
    • Interrupt 和 Abort
    • 線程狀態
    • 等待句柄
    • 同步環境
  • 使用多線程
    • 單元模式和Windows Forms
    • BackgroundWorker類
    • ReaderWriterLock類
    • 線程池
    • 異步委託
    • 計時器
    • 局部儲存
  • 高級話題
    • 非阻止同步
    • Wait和Pulse
    • Suspend和Resume
    • 終止線程


1、入門數據庫

1.     概述與概念

   C#支持經過多線程並行地執行代碼,一個線程有它獨立的執行路徑,可以與其它的線程同時地運行(單CPU多核)。一個C#客戶端程序(Console, WPF, or Windows Forms)開始於一個單線程,這個單線程是被CLR和操做系統(也稱爲「主線程」)自動建立的,並能夠經過建立額外的線程 組成多線程。這裏的一個簡單的例子及其輸出:express

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

 

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

主線程建立了一個新線程「t」,新線程它運行了一個重複打印字母"y"的方法,同時主線程重複打印字母「x」。安全

線程一旦開始,其 Islive屬性爲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 ('?');
}

image

 變量cycles的副本分別在各自的內存堆棧中建立,輸出也同樣,可預見,會有10個問號輸出。。
app

當兩個線程們引用了一些共同的對象實例的時候,他們會共享數據。下面是實例:

class ThreadTest
{
 bool done;  
 static void Main() 
{
   ThreadTest tt = new ThreadTest();  // Create a common instance (tt做爲他們共同的一個實例對象)
   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(),它們<span style="color:#ff0000;">共享了done字段,這個結果輸出的是一個"Done",而不是兩個</span>。

靜態字段提供了另外一種在線程間共享數據的方式,下面是一個以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; }
}

 

image

問題就是一個線程在判斷if塊的時候,正好另外一個線程正在執行WriteLine語句——在它將done設置爲true以前。

補救措施是當讀寫公共字段的時候,提供一個互斥;C#提供了lock語句來達到這個目的:

class ThreadSafe {
  static bool done;
  static readonly 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),一個線程等待,或者說被阻止(blocks), 直到那個鎖變的可用。在這種狀況下,就確保了在同一時刻只有一個線程能進入臨界區,因此"Done"只被打印了1次(以後done爲true)。代碼以如此方式在不肯定的多線程環境中被叫作線程安全


多線程中複雜而隱蔽錯誤的一個主要緣由是數據共享。儘管多線程是常常必須的,但儘量保持簡單點。

A thread, while blocked, doesn't consume CPU resources.(一個線程,阻塞的時候,不消耗CPU資源。)

(讓線程等一段時間再執行)


你(主線程)能夠等待另外一個線程結束後再執行 經過調用它(另外一線程)的Join方法,例如:
static void Main()
{
  Thread t = new Thread (Go);
  t.Start();
  t.Join();
  Console.WriteLine ("Thread t has ended!");
}
 
static void Go()
{
  for (int i = 0; i < 1000; i++) Console.Write ("y");
}

結果先打印1000次「y」,再打印Thread t has ended!,Join()可設置等待的時間,時間一到,其餘線程就會執行


Thread.Sleep pauses the current thread for a specified period:(阻止當前線程一個指定的時間)

Thread.Sleep (TimeSpan.FromHours (1));  // sleep for 1 hour
Thread.Sleep (500);                     // sleep for 500 milliseconds

 線程是如何工做的

   線程被一個線程協調程序管理着(一個CLR委託給操做系統的函數)。線程協調程序確保將全部活動的線程被分配適當的執行時間;而且那些等待或阻止的線程—好比說在互斥鎖中、或在用戶輸入,都是不消耗CPU時間的。

   在單核處理器的電腦中,線程協調程序完成一個時間片以後迅速地在活動的線程之間進行切換執行。這就致使「波濤洶涌」的行爲,例如在第一個例子,每次重複的X 或 Y 塊至關於分給線程的時間片。在Windows XP中時間片一般在10毫秒內選擇要比CPU開銷在處理線程切換的時候的消耗大的多。(即一般在幾微秒區間)

   在多核的電腦中,多線程被實現成混合時間片和真實的併發——不一樣的線程在不一樣的CPU上運行。這幾乎能夠確定仍然會出現一些時間切片, 因爲操做系統的須要服務本身的線程,以及一些其餘的應用程序。

   線程因爲外部因素(好比時間片)被中斷被稱爲被搶佔,在大多數狀況下,一個線程方面在被搶佔的那一時那一刻就失去了對它的控制權。

   線程 vs. 進程

    屬於一個單一的應用程序的全部的線程邏輯上被包含在一個進程中,進程指一個應用程序所運行的操做系統單元。

    線程於進程有某些類似的地方:如進程並行運行在電腦上同樣, 線程並行運行在單個進程。進程是徹底獨立於彼此的。而線程只是一個有限程度的隔離。線程與運行在相同程序中的其它線程共享(堆heap)內存,這就是線程爲什麼如此有用:一個線程能夠在後臺讀取數據,而另外一個線程能夠在前臺展示已讀取的數據。

  線程的使用和誤用

 多線程有許多用途,下面是最多見的用途:

一、保持一個快速響應的用戶界面

經過在一個並行的「worker」線程上運行耗時的任務,主UI線程能夠自由的繼續處理鍵盤和鼠標事件。

二、有效利用CPU的阻塞

多線程是有用的:當一個線程正在等待一個另外一臺計算機或硬件的響應時當一個線程由於執行任務而被阻塞時, 其餘線程能夠利用 計算機的未佔用資源。

三、並行編程

什麼時候使用多線程

    多線程程序通常被用來在後臺執行耗時的任務。主線程保持運行,而且工做線程作它的後臺工做。對於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類來建立, 

1、經過ThreadStart委託來指明方法從哪裏開始運行,下面是ThreadStart委託如何定義的:【也能夠不用】

public delegate void ThreadStart();  

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

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

2、一個線程能夠僅經過指定一個方法來方便的建立,而後C#指出線程開始的方法(不用明確使用委託也能夠,例如前面的例子)

 Thread t = new Thread (Go);    // No need to explicitly use ThreadStart

在這種狀況,ThreadStart被編譯器自動推斷出來,

3、另外一個快捷方式是使用一個lambda表達式或匿名方法:

static void Main()
{
  Thread t = new Thread ( () => Console.WriteLine ("Hello!") );
  t.Start();
}
線程有一個IsAlive屬性,在調用Start()以後直到線程結束以前一直爲true。一個線程一旦結束便不能從新開始了。
 

  將數據傳入ThreadStart中

最簡單的方法傳遞參數到一個線程:是執行一個lambda表達式,調用該方法所需的參數

static void Main()
{
  Thread t = new Thread ( () => Print ("Hello from t!") );
  t.Start();
}
 
static void Print (string message) 
{
  Console.WriteLine (message);
}

使用這種方法,您能夠將任意數量的參數傳遞給方法。你甚至能夠把整個實現包在一個多語句λ表達式中

new Thread (() =>
{
  Console.WriteLine ("I'm running on another thread!");
  Console.WriteLine ("This is so easy!");
}).Start();

使用匿名方法 一樣能夠
new Thread (delegate()
{
  ...
}).Start();

另外一種方法是:在Thread’s Start()方法中傳參數

static void Main()
{
  Thread t = new Thread (Print);
  t.Start ("Hello from t!");
}
 
static void Print (object messageObj)
{
  string message = (string) messageObj;   // We need to cast here
  Console.WriteLine (message);
}
由於線程的構造函數重載, 接受兩種委託
public delegate void ThreadStart();
public delegate void ParameterizedThreadStart (object obj);

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

Lambda表達式和捕獲變量:

正如咱們所見,一個lambda表達式是最強大的方式傳遞數據到一個線程。然而,您必須當心線程開始後變量的意外修改,由於這些變量都是共享的。例如,考慮如下:

for (int i = 0; i < 10; i++)
  new Thread (() => Console.Write (i)).Start();

輸出是非肯定的!  例如:224458891010  (後面的數不小於前面的數,在1-10之間的數,10次循環開了10個線程)

 Here’s a typical result:

The problem is that the i variable refers to the same memory location throughout the loop’s lifetime(循環週期時 兩個線程捕獲的是同一個內存地址區). Therefore, each thread calls Console.Write on a variable whose value may change as it is running!

 

The solution is to use a temporary variable as follows:

for (int i = 0; i < 10; i++)
{
  int temp = i;
  new Thread (() => Console.Write (temp)).Start();
}
變量temp位於每一個循環迭代。所以,每一個線程捕捉不一樣的內存位置(主線程捕獲i的內存區,子線程捕獲temp的內存區)這樣就沒問題。。(0123456789)

We can illustrate the problem in the earlier code more simply with the following example:

string text = "t1";
Thread t1 = new Thread ( () => Console.WriteLine (text) );
 
text = "t2";
Thread t2 = new Thread ( () => Console.WriteLine (text) );
 
t1.Start();
t2.Start();

Because both lambda expressions capture the same text variable, t2 is printed twice:

t2
t2
 

  命名線程

 線程能夠經過它的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);
  }
}

image

結果也有可能相反,不能保證哪一個先輸出。線程由操做系統來調度,每次哪一個線程先運行可能不一樣。


  前臺和後臺線程

 建立的線程默認爲前臺線程(而線程池中的線程老是後臺線程),這意味着任何一個前臺線程在運行都會保持程序存活。然然後臺線程不是。一旦全部的前臺線程結束,程序也就結束了,任何後臺線程也將忽然中止。。

一個線程的前臺/後臺狀態  與他的優先級或分配執行時間沒有關係。。

  改變線程從前臺到後臺不會以任何方式改變它在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。後臺線程終止的這種方式,使任何最後操做都被規避了,這種方式是不太合適的。好的方式是明確等待任何後臺工做線程完成後再結束程序,有兩種方法解決:

            a、對建立的線程調用Join()方法,讓其餘等待它的結束

            b、在線程池中的話,用一個事件等待來處理

在這兩種狀況下,你應該指定一個超時(timeout),因此能夠放棄一個叛離線程(出於某種緣由拒絕完成任務的線程)。

   擁有一個後臺工做線程是有益的,最直接的理由是它當提到結束程序它老是可能有最後的發言權。

   對於程序失敗退出的廣泛緣由就是存在「被忘記」的前臺線程。

  線程優先級

  線程的Priority 屬性肯定了線程相對於其它同一進程的活動的線程擁有多少執行時間,如下是級別:

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

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

注意:提高一個線程的優先級以前要仔細想一想—它可能致使其餘線程的資源缺少等問題

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

using (Process p = Process.GetCurrentProcess())
  p.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塊須要出如今每一個線程進入的方法內,至少要在產品程序中應該如此

相關文章
相關標籤/搜索