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不會回傳物件來幫助您隨后管理執行,另外,您必須在目標代碼中顯式處理例外-未處理的例外將使程式癱瘓,
異步委托
ThreadPool.QueueUserWorkItem沒有提供一種簡單的機制來在執行緒執行完畢后從執行緒取回回傳值,異步委托呼叫(簡稱異步委托)解決了這一問題,允許在兩個方向上傳遞任意數量的型別化引數,此外,異步委托上未處理的例外可以方便地在原始執行緒(或更準確地說是呼叫EndInvoke的執行緒)上重新拋出,因此不需要顯式處理,
不要將異步委托與異步方法(以Begin或End開頭的方法,例如File.BeginRead / File.EndRead)混淆,異步方法在外部遵循類似的協議,但是它們存在是為了解決更難的問題,我們將在C#4.0的第23章“簡而言之”中進行描述,
通過異步委托啟動作業任務的方法如下:
- 實體化一個以您要并行運行的方法為目標的委托(通常是預定義的Func委托之一),
- 在委托上呼叫BeginInvoke,保存其IAsyncResult回傳值, BeginInvoke立即回傳給呼叫者,然后,您可以在池執行緒正在作業時執行其他活動,
- 當需要結果時,在委托上呼叫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(50,50);View Code
(第二個值指示要分配給I / O完成埠的執行緒數,由APM使用,具體請參見C#4.0第23章的內容,)
默認值為每個內核一個執行緒,
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/59613.html
標籤:C#
下一篇:C#讀取靜態類常量屬性和值
