先交代下背景,寫《C#多執行緒之旅》這個系列文章主要是因為以下幾個原因:1.多執行緒在C/S和B/S架構中用得是非常多的;2.而且多執行緒的使用是非常複雜的,如果沒有用好,容易造成很多問題。
多執行緒,有利也有弊,使用需謹慎。
更多文章正在更新中,敬請期待……
C#多執行緒之旅(3)——執行緒池
程式碼下載
Thread_部落格園_cnblogs_jackson0714.zip
第一篇~第三篇的程式碼示例:
原始碼地址:https://github.com/Jackson0714/Threads
一、介紹
無論你什麼時候開始一個執行緒,幾百毫秒會花在整理一個新的local variable stack。每一個執行緒預設會消耗1MB的記憶體。執行緒池通過分享和回收執行緒來削減這些開銷,允許多執行緒被應用在一個非常顆粒級的級別而沒有效能損失。當充分利用多核系統去執行密集型計算的並行程式碼時這是非常有用的。
執行緒池也會線上程的總數量上保持一個限制,從而使執行緒能夠更平穩地執行。太多的執行緒將會造成管理負擔和使CPU快取是小,從而造成作業系統不能執行。一旦一個限制到達,job排隊等待直到另外一個完成才開始。這會使任意的並行應用程式成為可能,比如一個web server(同步方法是高階技巧,可以更高效地使用執行緒池中的執行緒)。
下面是幾種方式進入執行緒池:
- 通過Task Parallel Library(.NET 4.0)
- 通過呼叫ThreadPool.QueueUserWorkItem
- 通過asynchronous delegates
- 通過BackgroundWorkder
下面的結構直接使用執行緒池:
- WCF,Remoting,ASP.NET,ASMX Web Services application servers
- System.Timers.Timer and System.Threading.Timer
- Framework methods 由Async結束,比如WebClient(the event-based asynchronous pattern)和大部分的BeginXXX方法(the asynchronous programming model pattern)
- PLINQ
Task Parallel Library(TPL)和PLINQ是充分有效的和高等級的,甚至當執行緒池是不重要的時候,你也會想使用它們去協助處理多執行緒。
現在我們簡單的看一下我們怎樣使用Task類來實現一個簡單的執行線上程池上的委託。
當使用執行緒池時需要注意下面的事情:
- 你不能設定一個執行緒的名字,因為設定執行緒的名字將會使除錯更困難(當你在VS執行緒視窗中除錯時,即使你可以附加一個描述)。
- 執行緒池中的執行緒總是後臺執行緒(這通常不是問題)。
- 在應用程式的開始期間,阻塞一個執行緒可能會觸發一個延遲,除非你呼叫ThreadPool.SetMinThreads
你不能任意地改變池中的執行緒的優先順序–因為當它釋放會池中的時候,優先順序會被還原為正常狀態。
你可以通過屬性Thread.CurrentThread.IsThreadPoolThread的屬性查詢執行緒是否是正在執行的一個池中的執行緒
二、通過TPL進入執行緒池
你可以使用在TaskParallel Library中的Task類來輕鬆的進入執行緒池。這個Task類在Framework 4.0中有介紹:如果你對老的結構比較熟悉,考慮用非泛型的Task類替換ThreadPool.QueueUserWorkItem,將Asunchoronous delgates替換為泛型Task。最新的結構速度更快,更方便,而且更復雜。
為了使用非泛型的任務類,呼叫Task.Factory.StartNew方法,將方法傳進委託中。
Task.Factory.StartNew會返回一個Task物件,你可以使用它去監控這個task,比如,你可以呼叫它的wait方法等待它直到它完成。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
static void Main(string[] args) { Task task = Task.Factory.StartNew(Go); task.Wait(); Console.ReadKey(); } static void Go() { Console.WriteLine("From the thread pool start..."); Thread.Sleep(3000); Console.WriteLine("From the thread pool end"); } |
當你呼叫task的Wait 方法時,一個未處理的異常會很容易地重新丟擲到宿主執行緒上。(如果你不呼叫Wait方法而是放棄這個task,一個未處理的異常將會關閉掉這個程式)
泛型Task類是非泛型Task的子類。它讓你從這個已經完成執行的task中得到一個返回值。在下面的例子中,我們使用Task來下載一個web page
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
static void Main(string[] args) { Task task = Task.Factory.StartNew( () => DownloadString("http://www.baidu.com")); //呼叫其他方法 // //可以用task的Result的屬性來獲得task返回值。 //如果這個任務還在執行,當前的主執行緒將會被阻塞,直到這個任務完成。 string result = task.Result; } static string DownloadString(string uri) { using(var wc = new System.Net.WebClient()) { return wc.DownloadString(uri); } } |
Task Parallel Library有許多的功能,特別是提升多核處理器的效能。我們會在並行程式設計中繼續討論TPL。
三、不用TPL進入到執行緒池
如果你的應用程式是.NET Framework的早期版本(4.0之前的版本),你將不能使用TPL。你必須使用老的結構進入執行緒池:
ThreadPool.QueueUserWorkItem和asynchoronous delegates.兩者的不同點是asynchronous delegates讓你從執行緒那裡返回資料。Asynchronous delegates收集任何exception返回給呼叫者。
要使用QueueUserWorkItem,只需呼叫這個方法的執行線上程池上的委託。
1 2 3 4 5 6 7 8 9 10 11 |
static void Main(string[] args) { ThreadPool.QueueUserWorkItem(Go); ThreadPool.QueueUserWorkItem(Go, 123); Console.ReadKey(); } static void Go(object data) { Console.WriteLine("A from thread pool! " + data); } |
我們的目標方法Go,必須接收一個簡單object型別的引數(為了滿足waitCallBack委託)。這將提供一個簡單的方式傳遞資料到方法中,就像是ParameterizedThreadStart。不像Task,QueueUserWorkItem不會返回一個物件去幫助你之後管理執行。還有,你必須顯式在目標方法的程式碼中寫處理異常的程式碼–因為未處理的異常將會終止程式。
ThreadPool.QueueUserWorkItem沒有提供從一個已經完成的執行緒中得到它的返回值的機制。Asynchronous delegate invocations(asynchronous delegates for short)解決了這個問題,允許任何個數型別化的引數在兩個方向傳遞。此外,在asynchronous delegates上未處理的異常很方便地在原始執行緒上重新丟擲(更準確地說,這個執行緒叫做EndInvoke),因此不需要顯示處理。
不要混淆asynchronous delegates和asynchronous method(方法以Begin和End開頭的,比如File.BeginRead/File.EndRead)。Asynchronous methods表面上按照簡單的協議,但是它們的存在是為了解決一個更困難的問題。
下面是怎樣通過一個asynchronous delegate開始一個worker task:
- 例項化一個委託,該委託針對你想要並行執行的method(典型的是預定義Func delegates其中的一種)。
- 在delegate上呼叫BeginInvoke,儲存它的IAsyncResult返回值。BeginInvoke立即返回給呼叫者。當其他池中的執行緒正在執行的時候,你可以執行其他動作。
- 當你需要這個結果,在delegate上呼叫EndInvoke,傳遞已儲存的IAsyncResult物件。
在下面的例子中,我們使用一個asynchronous delegate invocation執行一個與主執行緒同時執行的簡單方法,這個方法返回一個字串的長度:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
static void Main(string[] args) { Func t = Go; IAsyncResult result = t.BeginInvoke("test", null, null); // // ... 這裡可以執行其他並行的任務 // int length = t.EndInvoke(result); Console.WriteLine("String lenth is: " + length); Console.ReadKey(); } static int Go(string messsage) { return messsage.Length; } |
EndInvoke做三件事情。第一,如果asynchronous delegate沒有完成執行,則一直等待它完成。第二,接收返回值(以及任何ref或者out引數)。第三,返回任何未處理的執行緒異常給呼叫它的執行緒。
注意:如果你用asynchronous delegate呼叫的方法沒有返回值,你在技術上需要呼叫EndInvoke。在實踐中,這是開放的辯論;沒有Endinvoke報警去管理處罰未編譯者!如果你選擇不去呼叫EndInvoke,然而,你需要考慮線上程的異常去避免靜默失敗。
當你呼叫BeginInvoke方法時,可以指定一個call back delegate-一個可以接收一個IAsyncResult 物件的方法,它會在委託方法完成後被自動呼叫這個允許正在發動的執行緒忘記asynchronous delegate,但它在call back結束時需要一點額外的工作。