C#線程 入門

Threading in C#

 

第一部分: 入門

介紹和概念

C#支持經過多線程並行執行代碼。線程是一個獨立的執行路徑,可以與其餘線程同時運行。C#客戶端程序(控制檯,WPF或Windows窗體)在CLR和操做系統自動建立的單個線程(「主」線程)中啓動,並經過建立其餘線程而成爲多線程。這是一個簡單的示例及其輸出:算法

全部示例均假定導入瞭如下名稱空間:shell

using System;
using System.Threading;
class ThreadTest

{
  static void Main()
  {
    Thread t = new Thread (WriteY);          // Kick off a new thread
    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,直到線程結束爲止。當傳遞給線程構造函數的委託完成執行時,線程結束。一旦結束,線程將沒法從新啓動。編程

 1 static void Main() 
 2 {
 3   new Thread (Go).Start();      // Call Go() on a new thread
 4   Go();                         // Call Go() on the main thread
 5 }
 6  
 7 static void Go()
 8 {
 9   // Declare and use a local variable - 'cycles'
10   for (int cycles = 0; cycles < 5; cycles++) Console.Write ('?');
11 }

 

??????????

在每一個線程的內存堆棧上建立一個單獨的cycles變量副本,所以,能夠預見的是,輸出爲十個問號。緩存

 

若是線程具備對同一對象實例的公共引用,則它們共享數據。例如:安全

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字段。這致使「完成」打印一次而不是兩次:服務器

完成cookie

靜態字段提供了另外一種在線程之間共享數據的方法。這是同一示例,其做爲靜態字段完成了:網絡

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

 

這兩個示例都說明了另外一個關鍵概念:線程安全的概念(或更確切地說,缺少安全性)。輸出其實是不肯定的:「完成」有可能(儘管不太可能)打印兩次。可是,若是咱們在Go方法中交換語句的順序,則兩次打印完成的機率會大大提升:多線程

static void Go()
{
  if (!done) { Console.WriteLine ("Done"); done = true; }
}
View Code

完成

完成(一般!)

問題在於,一個線程能夠評估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; }
    }
  }
}
View Code

 

當兩個線程同時爭用一個鎖(在這種狀況下爲鎖櫃)時,一個線程將等待或阻塞,直到鎖可用爲止。在這種狀況下,能夠確保一次只有一個線程能夠輸入代碼的關鍵部分,而且「完成」將僅打印一次。以這種方式受到保護的代碼(在多線程上下文中不受不肯定性的影響)被稱爲線程安全的。共享數據是形成多線程複雜性和模糊錯誤的主要緣由。儘管一般是必不可少的,但保持儘量簡單是值得的。線程雖然被阻止,但不會消耗CPU資源。

Join and Sleep

您能夠經過調用其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");
}
View Code

這將打印「 y」 1,000次,而後顯示「線程t已結束!」。緊接着。您能夠在調用Join時包含一個超時(以毫秒爲單位)或做爲TimeSpan。而後,若是線程結束,則返回true;若是超時,則返回false。

 

Thread.Sleep將當前線程暫停指定的時間:

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

  

在等待睡眠或加入時,線程被阻塞,所以不消耗CPU資源。

 

Thread.Sleep(0)當即放棄線程的當前時間片,自動將CPU移交給其餘線程。 Framework 4.0的新Thread.Yield()方法具備相同的做用-只是它只放棄運行在同一處理器上的線程。

 

Sleep(0)或Yield在生產代碼中偶爾用於進行高級性能調整。它也是幫助發現線程安全問題的出色診斷工具:若是在代碼中的任意位置插入Thread.Yield()會破壞程序,則幾乎確定會出現錯誤。

 線程如何工做 

 

多線程由線程調度程序在內部進行管理,這是CLR一般委託給操做系統的功能。線程調度程序確保爲全部活動線程分配適當的執行時間,而且正在等待或阻塞的線程(例如,排他鎖或用戶輸入)不會浪費CPU時間。

在單處理器計算機上,線程調度程序執行時間切片-在每一個活動線程之間快速切換執行。在Windows下,時間片一般在數十毫秒的區域中-遠大於在一個線程與另外一個線程之間實際切換上下文時的CPU開銷(一般在幾微秒的區域)。

在多處理器計算機上,多線程是經過時間片和真正的併發實現的,其中不一樣的線程在不一樣的CPU上同時運行代碼。幾乎能夠確定,因爲操做系統須要服務本身的線程以及其餘應用程序的線程,所以還會有一些時間片。

當線程的執行因爲外部因素(例如時間分段)而中斷時,能夠說該線程被搶佔。在大多數狀況下,線程沒法控制其搶佔的時間和地點。

 線程與進程

線程相似於您的應用程序在其中運行的操做系統進程。正如進程在計算機上並行運行同樣,線程在單個進程中並行運行。流程彼此徹底隔離;線程的隔離度有限。特別是,線程與在同一應用程序中運行的其餘線程共享(堆)內存。這部分是爲何線程有用的緣由:例如,一個線程能夠在後臺獲取數據,而另外一個線程能夠在數據到達時顯示數據

線程的使用和濫用

多線程有不少用途。這是最多見的:

 

維護響應式用戶界面

 

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

有效利用本來被阻塞的CPU

 

當線程正在等待另外一臺計算機或硬件的響應時,多線程頗有用。當一個線程在執行任務時被阻塞時,其餘線程能夠利用本來沒有負擔的計算機。

 

並行編程

 

若是以「分而治之」策略在多個線程之間共享工做負載,則執行密集計算的代碼能夠在多核或多處理器計算機上更快地執行(請參閱第5部分)。

 

投機執行

 

在多核計算機上,有時能夠經過預測可能須要完成的事情而後提早進行來提升性能。 LINQPad使用此技術來加快新查詢的建立。一種變化是並行運行許多不一樣的算法,這些算法均可以解決同一任務。不論哪個先得到「勝利」,當您不知道哪一種算法執行最快時,這纔有效。

 

容許同時處理請求

 

在服務器上,客戶端請求能夠同時到達,所以須要並行處理(若是使用ASP.NET,WCF,Web服務或遠程處理,.NET Framework會爲此自動建立線程)。這在客戶端上也頗有用(例如,處理對等網絡-甚至來自用戶的多個請求)。

 

使用ASP.NET和WCF之類的技術,您可能甚至不知道多線程正在發生-除非您在沒有適當鎖定的狀況下訪問共享數據(也許經過靜態字段),不然會破壞線程安全性。

 

線程還附帶有字符串。最大的問題是多線程會增長複雜性。有不少線程自己並不會帶來不少複雜性。確實是線程之間的交互(一般是經過共享數據)。不管交互是不是有意的,這都適用,而且可能致使較長的開發週期以及對間歇性和不可複製錯誤的持續敏感性。所以,必須儘可能減小交互,並儘量地堅持簡單且通過驗證的設計。本文主要側重於處理這些複雜性。刪除互動,無需多說!

 

好的策略是將多線程邏輯封裝到可重用的類中,這些類能夠獨立檢查和測試。框架自己提供了許多更高級別的線程結構,咱們將在後面介紹。

 

線程化還會在調度和切換線程時(若是活動線程多於CPU內核)會致使資源和CPU成本的增長,而且還會產生建立/拆除的成本。多線程並不老是能夠加快您的應用程序的速度-若是使用過多或使用不當,它甚至可能減慢其速度。例如,當涉及大量磁盤I / O時,讓幾個工做線程按順序運行任務比一次執行10個線程快得多。 (在「使用等待和脈衝發送信號」中,咱們描述瞭如何實現僅提供此功能的生產者/消費者隊列。)

建立和啓動線程

正如咱們在簡介中所看到的,線程是使用Thread類的構造函數建立的,並傳入ThreadStart委託,該委託指示應從何處開始執行。定義ThreadStart委託的方法以下:

public delegate void ThreadStart();

在線程上調用Start,而後將其設置爲運行。線程繼續執行,直到其方法返回爲止,此時線程結束。這是使用擴展的C#語法建立TheadStart委託的示例:

 1 class ThreadTest
 2 {
 3   static void Main() 
 4   {
 5     Thread t = new Thread (new ThreadStart (Go));
 6  
 7     t.Start();   // Run Go() on the new thread.
 8     Go();        // Simultaneously run Go() in the main thread.
 9   }
10  
11   static void Go()
12   {
13     Console.WriteLine ("hello!");
14   }
15 }
View Code

 

在此示例中,線程t在主線程調用Go()的同一時間執行Go()。結果是兩個接近即時的問候。

 

經過僅指定一個方法組,並容許C#推斷ThreadStart委託,能夠更方便地建立線程:

 Thread t = new Thread (Go);  //無需顯式使用ThreadStart

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

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

 

將數據傳遞給線程

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

 1 static void Main()
 2 {
 3   Thread t = new Thread ( () => Print ("Hello from t!") );
 4   t.Start();
 5 }
 6  
 7 static void Print (string message) 
 8 {
 9   Console.WriteLine (message);
10 }

 

使用這種方法,您能夠將任意數量的參數傳遞給該方法。您甚至能夠將整個實現包裝在多語句lambda中:

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

 

您可使用匿名方法在C#2.0中幾乎輕鬆地執行相同的操做:

new Thread (delegate()
{
  ...
}).Start();

另外一種技術是將參數傳遞給Thread的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);
}

 

之因此可行,是由於Thread的構造函數被重載爲接受兩個委託之一:

public delegate void ThreadStart();
public delegate void ParameterizedThreadStart (object obj);

  

ParameterizedThreadStart的侷限性在於它僅接受一個參數。並且因爲它是object類型的,所以一般須要強制轉換。

Lambda表達式和捕獲的變量

如咱們所見,lambda表達式是將數據傳遞到線程的最強大的方法。可是,您必須當心在啓動線程後意外修改捕獲的變量,由於這些變量是共享的。例如,考慮如下內容:
for (int i = 0; i < 10; i++)
  new Thread (() => Console.Write (i)).Start();

  輸出是不肯定的!這是一個典型的結果:

0223557799

問題在於,i變量在循環的整個生命週期中都指向相同的內存位置。所以,每一個線程都會在變量上調用Console.Write,該變量的值可能會隨着運行而改變!

 

這相似於咱們在C#4.0的第八章「捕獲變量」中描述的問題。問題不在於多線程,而是與C#捕獲變量的規則有關(在for和foreach循環的狀況下這是不但願的)。

 

 解決方案是使用以下臨時變量:

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

  

如今,可變溫度是每一個循環迭代的局部變量。所以,每一個線程捕獲一個不一樣的內存位置,這沒有問題。咱們能夠經過如下示例更簡單地說明早期代碼中的問題:

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

  

由於兩個lambda表達式都捕獲相同的文本變量,因此t2被打印兩次

t2
t2

命名線程

每一個線程都有一個Name屬性,能夠設置該屬性以利於調試。這在Visual Studio中特別有用,由於線程的名稱顯示在「線程窗口」和「調試位置」工具欄中。您只需設置一個線程名稱便可;稍後嘗試更改它會引起異常。

 

靜態Thread.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);
  }
}

 

  

 
 

前臺線程和後臺線程

默認狀況下,您顯式建立的線程是前臺線程。只要前臺線程中的任何一個正在運行,它就可使應用程序保持活動狀態,然後臺線程則不會。一旦全部前臺線程完成,應用程序結束,全部仍在運行的後臺線程終止。

 

線程的前臺/後臺狀態與其優先級或執行時間的分配無關。

 

您可使用其IsBackground屬性查詢或更改線程的背景狀態。這是一個例子:

class PriorityTest
{
  static void Main (string[] args)
  {
    Thread worker = new Thread ( () => Console.ReadLine() );
    if (args.Length > 0) worker.IsBackground = true;
    worker.Start();
  }
}

若是不帶任何參數調用此程序,則工做線程將處於前臺狀態,並將在ReadLine語句上等待用戶按Enter。同時,主線程退出,可是應用程序繼續運行,由於前臺線程仍然處於活動狀態。

 

另外一方面,若是將參數傳遞給Main(),則會爲工做程序分配背景狀態,而且在主線程結束(終止ReadLine)時,程序幾乎當即退出。

 

當進程以這種方式終止時,將規避後臺線程執行堆棧中的全部finally塊。若是您的程序最終使用(或使用)塊來執行清理工做(例如釋放資源或刪除臨時文件),則會出現問題。爲了不這種狀況,您能夠在退出應用程序後顯式等待此類後臺線程。

有兩種方法能夠實現此目的:

 

  • 若是您本身建立了線程,請在該線程上調用Join。
  • 若是您使用的是共享線程,請使用事件等待句柄。

在這兩種狀況下,您都應指定一個超時時間,以便在因爲某種緣由而拒絕完成的叛逆線程時能夠放棄它。這是您的備份退出策略:最後,您但願您的應用程序關閉-無需用戶從任務管理器中尋求幫助!

 

若是用戶使用任務管理器強制結束.NET進程,則全部線程都「掉線」,就好像它們是後臺線程同樣。這是觀察到的,而不是記錄的行爲,而且它可能因CLR和操做系統版本而異。

 

前景線程不須要這種處理,可是您必須注意避免可能致使線程沒法結束的錯誤。應用程序沒法正常退出的常見緣由是活動的前臺線程的存在。

線程優先級

線程的「優先級」屬性肯定相對於操做系統中其餘活動線程而言,執行時間的長短以下:

 

枚舉ThreadPriority {最低,低於正常,正常,高於正常,最高}

僅在同時激活多個線程時,這纔有意義。

 

在提升線程的優先級以前,請仔細考慮-這可能致使諸如其餘線程的資源匱乏之類的問題。

 

提高線程的優先級並使其沒法執行實時工做,由於它仍然受到應用程序進程優先級的限制。要執行實時工做,您還必須使用System.Diagnostics中的Process類提升流程優先級(咱們沒有告訴您如何執行此操做):

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

  

實際上,ProcessPriorityClass.High比最高優先級低了一個等級:實時。將進程優先級設置爲「實時」會指示OS,您從不但願該進程將CPU時間浪費給另外一個進程。若是您的程序進入意外的無限循環,您甚至可能會發現操做系統已鎖定,只剩下電源按鈕能夠拯救您!所以,「高」一般是實時應用程序的最佳選擇。

 

若是您的實時應用程序具備用戶界面,則提升進程優先級將給屏幕更新帶來過多的CPU時間,從而減慢整個計算機的速度(尤爲是在UI複雜的狀況下)。下降主線程的優先級並提升進程的優先級可確保實時線程不會因屏幕重繪而被搶佔,但不會解決使其餘應用程序耗盡CPU時間的問題,由於操做系統仍會分配整個過程的資源不成比例。理想的解決方案是使實時工做程序和用戶界面做爲具備不一樣進程優先級的單獨應用程序運行,並經過遠程處理或內存映射文件進行通訊。內存映射文件很是適合此任務。簡而言之,咱們將在C#4.0的第14和25章中解釋它們的工做原理。

 

即便提升了流程優先級,託管環境在處理嚴格的實時需求方面的適用性也受到限制。除了由自動垃圾收集引發的延遲問題外,操做系統(甚至對於非託管應用程序)可能還會帶來其餘挑戰,而這些挑戰最好經過專用硬件或專用實時平臺來解決。

異常處理

建立線程時,做用域中的任何try / catch / finally塊都與線程開始執行時無關。考慮如下程序:

public static void Main()
{
  try
  {
    new Thread (Go).Start();
  }
  catch (Exception ex)
  {
    // We'll never get here!
    Console.WriteLine ("Exception!");
  }
}
 
static void Go() { throw null; }   // Throws a NullReferenceException

此示例中的try / catch語句無效,而且新建立的線程將受到未處理的NullReferenceException的阻礙。當您認爲每一個線程都有一個獨立的執行路徑時,此行爲頗有意義。

補救措施是將異常處理程序移至Go方法中:

public static void Main()
{
   new Thread (Go).Start();
}
 
static void Go()
{
  try
  {
    // ...
    throw null;    // The NullReferenceException will get caught below
    // ...
  }
  catch (Exception ex)
  {
    // Typically log the exception, and/or signal another thread
    // that we've come unstuck
    // ...
  }
}

  

在生產應用程序中的全部線程進入方法上都須要一個異常處理程序,就像在主線程上同樣(一般在執行堆棧中處於更高級別)。未處理的異常會致使整個應用程序關閉。與一個醜陋的對話!

在編寫此類異常處理塊時,不多會忽略該錯誤:一般,您會記錄異常的詳細信息,而後顯示一個對話框,容許用戶自動將這些詳細信息提交到您的Web服務器。而後,您可能會關閉該應用程序-由於該錯誤有可能破壞了程序的狀態。可是,這樣作的代價是用戶將丟失其最近的工做-例如打開的文檔。

WPF和Windows Forms應用程序的「全局」異常處理事件(Application.DispatcherUnhandledException和Application.ThreadException)僅針對在主UI線程上引起的異常觸發。您仍然必須手動處理工做線程上的異常。

 AppDomain.CurrentDomain.UnhandledException在任何未處理的異常上觸發,但沒有提供防止應用程序隨後關閉的方法。可是,在某些狀況下,您不須要處理工做線程上的異常,由於.NET Framework會爲您處理異常。這些將在接下來的部分中介紹,分別是:

  • 異步委託
  • 後臺工做者
  • 任務並行庫(適用條件)

 

線程池

每當啓動線程時,都會花費數百微秒來組織諸如新鮮的私有局部變量堆棧之類的事情。每一個線程(默認狀況下)也消耗大約1 MB的內存。線程池經過共享和回收線程來減小這些開銷,從而容許在很是細粒度的級別上應用多線程,而不會影響性能。當利用多核處理器以「分而治之」的方式並行執行計算密集型代碼時,這頗有用。

線程池還限制了將同時運行的工做線程總數。過多的活動線程限制了操做系統的管理負擔,並使CPU緩存無效。一旦達到限制,做業將排隊並僅在另外一個做業完成時纔開始。這使任意併發的應用程序(例如Web服務器)成爲可能。 (異步方法模式是一種高級技術,經過高效利用池化線程來進一步實現這一點;咱們在C#4.0的第23章「 Nutshell」中對此進行了描述)。

有多種進入線程池的方法:

  1. 經過任務並行庫(來自Framework 4.0)
  2. 經過調用ThreadPool.QueueUserWorkItem
  3. 經過異步委託
  4. 經過BackgroundWorker

如下構造間接使用線程池:

  • WCF,遠程,ASP.NET和ASMX Web服務應用程序服務器
  • System.Timers.Timer和System.Threading.Timer
  • 以Async結尾的框架方法,例如WebClient上的框架方法(基於事件的異步模式),以及大多數BeginXXX方法(異步編程模型模式)
  • PLINQ

任務並行庫(TPL)和PLINQ具備足夠的功能和高級功能,即便在線程池不重要的狀況下,您也但願使用它們來協助多線程。咱們將在第5部分中詳細討論這些內容。如今,咱們將簡要介紹如何使用Task類做爲在池線程上運行委託的簡單方法。

使用池線程時須要注意如下幾點:

  • 您沒法設置池線程的名稱,從而使調試更加困難(儘管您能夠在Visual Studio的「線程」窗口中進行調試時附加說明)。
  • 池線程始終是後臺線程(這一般不是問題)。
  • 除非您調用ThreadPool.SetMinThreads(請參閱優化線程池),不然阻塞池中的線程可能會在應用程序的早期階段觸發額外的延遲。
  • 您能夠自由更改池線程的優先級-在釋放回池時,它將恢復爲正常。

 

您能夠經過Thread.CurrentThread.IsThreadPoolThread屬性查詢當前是否在池化線程上執行。

經過TPL進入線程池

您可使用「任務並行庫」中的「任務」類輕鬆地輸入線程池。 Task類是在Framework 4.0中引入的:若是您熟悉較早的構造,請考慮將非通用Task類替換爲ThreadPool.QueueUserWorkItem,而將通用Task <TResult>替換爲異步委託。與舊版本相比,新版本的結構更快,更方便且更靈活。

 

要使用非泛型Task類,請調用Task.Factory.StartNew,並傳入目標方法的委託:

static void Main()    // The Task class is in System.Threading.Tasks
{
  Task.Factory.StartNew (Go);
}
 
static void Go()
{
  Console.WriteLine ("Hello from the thread pool!");
}

  

Task.Factory.StartNew返回一個Task對象,您可使用該對象來監視任務-例如,您能夠經過調用其Wait方法來等待它完成。

 

調用任務的Wait方法時,全部未處理的異常均可以方便地從新拋出到主機線程中。 (若是您不調用Wait而是放棄任務,則未處理的異常將像普通線程同樣關閉進程。)

 

通用Task <TResult>類是非通用Task的子類。它使您能夠在完成執行後從任務中獲取返回值。在下面的示例中,咱們使用Task <TResult>下載網頁:

static void Main()
{
  // Start the task executing:
  Task<string> task = Task.Factory.StartNew<string>
    ( () => DownloadString ("http://www.linqpad.net") );
 
  // We can do other work here and it will execute in parallel:
  RunSomeOtherMethod();
 
  // When we need the task's return value, we query its Result property:
  // If it's still executing, the current thread will now block (wait)
  // until the task finishes:
  string result = task.Result;
}
 
static string DownloadString (string uri)
{
  using (var wc = new System.Net.WebClient())
    return wc.DownloadString (uri);
}

 

(突出顯示<string>類型的參數是爲了清楚:若是咱們省略它,則能夠推斷出它。)

查詢包含在AggregateException中的任務的Result屬性時,全部未處理的異常都會自動從新拋出。可是,若是您沒法查詢其Result屬性(而且不調用Wait),則任何未處理的異常都會使該過程失敗。

任務並行庫具備更多功能,特別適合利用多核處理器。咱們將在第5部分中繼續討論TPL。

不經過TPL進入線程池

若是目標是.NET Framework的早期版本(4.0以前),則不能使用任務並行庫。相反,您必須使用一種較舊的結構來輸入線程池:ThreadPool.QueueUserWorkItem和異步委託。二者之間的區別在於異步委託使您能夠從線程返回數據。異步委託也將任何異常封送回調用方。

QueueUserWorkItem

要使用QueueUserWorkItem,只需使用要在池線程上運行的委託調用此方法:

static void Main()
{
  ThreadPool.QueueUserWorkItem (Go);
  ThreadPool.QueueUserWorkItem (Go, 123);
  Console.ReadLine();
}
 
static void Go (object data)   // data will be null with the first call.
{
  Console.WriteLine ("Hello from the thread pool! " + data);
}
Hello from the thread pool!
Hello from the thread pool! 123

咱們的目標方法Go必須接受單個對象參數(以知足WaitCallback委託)。就像使用ParameterizedThreadStart同樣,這提供了一種將數據傳遞給方法的便捷方法。與Task不一樣,QueueUserWorkItem不會返回對象來幫助您隨後管理執行。另外,您必須在目標代碼中顯式處理異常-未處理的異常將使程序癱瘓。

異步委託

ThreadPool.QueueUserWorkItem沒有提供一種簡單的機制來在線程執行完畢後從線程取回返回值。異步委託調用(簡稱異步委託)解決了這一問題,容許在兩個方向上傳遞任意數量的類型化參數。此外,異步委託上未處理的異常能夠方便地在原始線程(或更準確地說是調用EndInvoke的線程)上從新拋出,所以不須要顯式處理。

不要將異步委託與異步方法(以Begin或End開頭的方法,例如File.BeginRead / File.EndRead)混淆。異步方法在外部遵循相似的協議,可是它們存在是爲了解決更難的問題,咱們將在C#4.0的第23章「簡而言之」中進行描述。

經過異步委託啓動工做任務的方法以下:

  1. 實例化一個以您要並行運行的方法爲目標的委託(一般是預約義的Func委託之一)。
  2. 在委託上調用BeginInvoke,保存其IAsyncResult返回值。     BeginInvoke當即返回給調用者。而後,您能夠在池線程正在工做時執行其餘活動。
  3. 當須要結果時,在委託上調用EndInvoke,傳入保存的IAsyncResult對象。

在下面的示例中,咱們使用異步委託調用與主線程併發執行,主線程是一種返回字符串長度的簡單方法:

 

static void Main()
{
  Func<string, int> method = Work;
  IAsyncResult cookie = method.BeginInvoke ("test", null, null);
  //
  // ... here's where we can do other work in parallel...
  //
  int result = method.EndInvoke (cookie);
  Console.WriteLine ("String length is: " + result);
}
static int Work (string s) { return s.Length; }

  

EndInvoke作三件事。首先,它會等待異步委託完成執行(若是還沒有執行)。其次,它接收返回值(以及任何ref或out參數)。第三,它將全部未處理的工做程序異常拋出回調用線程。

若是您使用異步委託調用的方法沒有返回值,則仍然(在技術上)有義務調用EndInvoke。實際上,這是有爭議的。沒有EndInvoke警察對違規者進行處罰!可是,若是您選擇不調用EndInvoke,則須要考慮worker方法上的異常處理,以免無提示的失敗。

您還能夠在調用BeginInvoke時指定一個回調委託-一種接受IAsyncResult對象的方法,該方法在完成後會自動調用。這容許煽動線程「忘記」異步委託,可是在回調端須要一些額外的工做:

static void Main()
{
  Func<string, int> method = Work;
  method.BeginInvoke ("test", Done, method);
  // ...
  //
}
 
static int Work (string s) { return s.Length; }
 
static void Done (IAsyncResult cookie)
{
  var target = (Func<string, int>) cookie.AsyncState;
  int result = target.EndInvoke (cookie);
  Console.WriteLine ("String length is: " + result);
}
View Code

BeginInvoke的最後一個參數是填充IAsyncResult的AsyncState屬性的用戶狀態對象。它能夠包含您喜歡的任何內容;在這種狀況下,咱們使用它將方法委託傳遞給完成回調,所以咱們能夠在其上調用EndInvoke。

優化線程池

線程池從其池中的一個線程開始。分配任務後,池管理器會「注入」新線程以應對額外的併發工做負載(最大限制)。在足夠長時間的不活動以後,若是池管理器懷疑這樣作會致使更好的吞吐量,則能夠「退出」線程。

您能夠經過調用ThreadPool.SetMaxThreads;來設置池將建立的線程的上限。默認值爲:

  • 32位環境中Framework 4.0中的1023
  • 在64位環境中的Framework 4.0中爲32768
  • 框架3.5中的每一個核心250個
  • Framework 2.0中每一個內核25個

(這些數字可能會因硬件和操做系統而異。)之因此有不少緣由,是爲了確保某些線程被阻塞(在等待某種條件(例如,來自遠程計算機的響應)時處於空閒狀態)的進度。

您還能夠經過調用ThreadPool.SetMinThreads設置下限。下限的做用是微妙的:這是一種高級優化技術,它指示池管理器在達到下限以前不要延遲線程的分配。當線程被阻塞時,提升最小線程數可提升併發性(請參見側欄)。

默認的下限是每一個處理器內核一個線程-容許所有CPU利用率的最小值。可是,在服務器環境(例如IIS下的ASP.NET)上,下限一般要高得多-多達50個或更多。

 

最小線程數如何工做?

 

實際上,將線程池的最小線程數增長到x並不會實際上強制當即建立x個線程-線程僅根據須要建立。相反,它指示池管理器在須要它們時當即最多建立x個線程。那麼,問題是,爲何在須要時線程池會延遲建立線程的時間呢?

答案是防止短暫的短暫活動致使線程的徹底分配,從而忽然膨脹應用程序的內存空間。爲了說明這一點,請考慮運行一個客戶端應用程序的四核計算機,該應用程序一次可處理40個任務。若是每一個任務執行10毫秒的計算,則假設工做在四個核心之間分配,整個任務將在100毫秒內結束。理想狀況下,咱們但願40個任務剛好在四個線程上運行:

  • 減小一點,咱們就不會充分利用這四個核心。
  • 再有,咱們將浪費內存和CPU時間來建立沒必要要的線程。

這正是線程池的工做方式。只要將線程數與內核數進行匹配,只要有效地使用了線程(在這種狀況下就是這樣),程序就能夠在不影響性能的狀況下保留較小的內存佔用。

可是如今假設,每一個任務而不是工做10毫秒,而是查詢Internet,在本地CPU空閒時等待半秒以響應。池管理器的線程經濟策略崩潰了;如今建立更多線程會更好,所以全部Internet查詢均可以同時發生。

幸運的是,池管理器有一個備份計劃。若是其隊列保持靜止狀態超過半秒,它將經過建立更多線程(每半秒一個)來響應,直至達到線程池的容量。

延遲的半秒是一把兩刃劍。一方面,這意味着一次短暫的短暫活動不會使程序忽然消耗掉沒必要要的40 MB(或更多)內存。另外一方面,當池中的線程阻塞時,例如查詢數據庫或調用WebClient.DownloadFile時,它可能沒必要要地延遲事情。所以,能夠經過調用SetMinThreads來告訴池管理器不要延遲前x個線程的分配:

ThreadPool.SetMinThreads(5050);
View Code

(第二個值指示要分配給I / O完成端口的線程數,由APM使用,具體請參見C#4.0第23章的內容。)

默認值爲每一個內核一個線程。

相關文章
相關標籤/搜索