Threading in C# 第一部分: 入門 介紹和概念 C#支持通過多線程並行執行代碼。線程是一個獨立的執行路徑,能夠與其他線程同時運行。C#客戶端程式(控制台,WPF或Windows窗體)在CLR和操作系統自動創建的單個線程(“主”線程)中啟動,並通過創建其他線程而成為多線程。這是一個簡單的 ...
Threading in C#
第一部分: 入門
介紹和概念
C#支持通過多線程並行執行代碼。線程是一個獨立的執行路徑,能夠與其他線程同時運行。C#客戶端程式(控制台,WPF或Windows窗體)在CLR和操作系統自動創建的單個線程(“主”線程)中啟動,並通過創建其他線程而成為多線程。這是一個簡單的示例及其輸出:
所有示例均假定導入了以下名稱空間:
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欄位。這導致“完成”列印一次而不是兩次:
完成
靜態欄位提供了另一種線上程之間共用數據的方法。這是同一示例,其作為靜態欄位完成了:
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”中對此進行了描述)。
有多種進入線程池的方法:
- 通過任務並行庫(來自Framework 4.0)
- 通過調用ThreadPool.QueueUserWorkItem
- 通過非同步委托
- 通過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不會返回對象來幫助您隨後管理執行。另外,您必須在目標代碼中顯式處理異常-未處理的異常將使程式癱瘓。
非同步委托