????????
???????
目錄
1.通過TPL使用線程池? ??
2.不使用TPL進入線程池的辦法
異步委托
3.線程池優化技術
最小線程數的工作原理
?????????每當啟動一個新線程時,系統都需要花費數百微秒來分配資源,例如創建獨立的局部變量棧空間。默認情況下,每個線程還會占用約1MB內存。線程池通過共享和回收線程來消除這些開銷,使得多線程技術可以應用于非常細粒度的場景而不會造成性能損失。這在利用多核處理器以"分而治之"方式并行執行計算密集型代碼時尤為有用。
線程池還會限制同時運行的線程總數。過多的活動線程會給操作系統帶來管理負擔,并導致CPU緩存失效。一旦達到限制,新任務將進入隊列,只有當前線程完成任務后才能啟動。這使得高并發應用(如Web服務器)的實現成為可能。(異步方法模式是一種更高級的技術,它能更高效地利用線程池中的線程,我們會在后面文章講解)。
進入線程池有多種方式:
- Task Parallel Library (Framework 4.0)
- ThreadPool.QueueUserWorkItem
- asynchronous delegates
- BackgroundWorker
以下組件會間接使用線程池:
? WCF、遠程處理(Remoting)、ASP.NET和ASMX Web服務應用服務器
? System.Timers.Timer和System.Threading.Timer計時器
? 以Async結尾的框架方法(如WebClient基于事件的異步模式)
? 大多數BeginXXX方法(異步編程模型模式)
? PLINQ并行查詢
任務并行庫(TPL)和PLINQ功能強大且抽象層次高,即使不考慮線程池優勢也值得用于多線程開發。使用線程池時需注意以下事項:
-
無法設置線程池線程的Name屬性,這會增加調試難度(但在Visual Studio線程窗口中可附加描述信息)
-
線程池線程默認都是后臺線程(通常不影響使用)
-
在應用初期阻塞線程池線程可能導致額外延遲,除非調用ThreadPool.SetMinThreads(參見"優化線程池"章節)
-
可臨時修改線程池線程優先級,但釋放回池后優先級會自動重置為普通級別
可通過Thread.CurrentThread.IsThreadPoolThread屬性檢測當前是否運行在線程池線程上。
1.通過TPL使用線程池? ??
????????使用任務并行庫(TPL)中的Task類可以輕松進入線程池。Task類是在.NET 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類型對象,你可以使用這個對象管理這個任務,比如你可以用Waith方法等待任務完成。
注意:當調用任務的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);
}
當查詢任務的Result屬性時,任何未處理的異常都會自動重新拋出(封裝在AggregateException中)。但如果不查詢Result屬性(也不調用Wait方法),未處理的異常將會導致進程崩潰。
任務并行庫(TPL)功能遠不止于此,它特別適合利用多核處理器優勢。我會在后續文章專門討論TPL的更多功能。
2.不使用TPL進入線程池的辦法
????????如果您的開發目標是.NET Framework 4.0之前的版本,則無法使用任務并行庫(TPL)。此時必須改用以下傳統方式進入線程池:ThreadPool.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必須接受單個object參數(以滿足WaitCallback委托)。這提供了傳遞數據的便捷方式,類似于ParameterizedThreadStart。但與Task不同,QueueUserWorkItem不會返回對象來幫助后續執行管理。此外,您必須顯式處理目標代碼中的異常——未處理的異常將導致程序崩潰。
異步委托
????????ThreadPool.QueueUserWorkItem未提供簡單機制來獲取線程執行完成后的返回值。異步委托調用(簡稱異步委托)解決了這個問題,允許任意數量的類型化參數雙向傳遞。更重要的是,異步委托上的未處理異常會便捷地重新拋回到原始線程(更準確地說,是調用EndInvoke的線程),因此不需要顯式處理。
以下是使用異步委托啟動工作任務的步驟:
-
實例化指向要在并行中運行方法的委托(通常使用預定義的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,則必須自行處理工作方法的異常,避免靜默失敗
高級用法:
調用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);
}
BeginInvoke 的最后一個參數是用戶狀態對象,該對象會填充 IAsyncResult 的 AsyncState 屬性。這個參數可以傳遞任意您需要的數據;在本例中,我們用它向完成回調傳遞方法委托,以便我們能夠對其調用 EndInvoke。
3.線程池優化技術
????????線程池初始時僅包含一個線程。當任務被分配時,池管理器會"注入"新線程以應對額外的并發工作負載,直至達到最大限制。在持續空閑足夠長時間后,如果池管理器判斷減少線程能提升吞吐量,則可能"回收"多余線程。
您可以通過ThreadPool.SetMaxThreads設置線程池創建線程的上限,各版本默認值為:
????????? Framework 4.0(32位環境):1023個線程
????????? Framework 4.0(64位環境):32768個線程
????????? Framework 3.5:每個核心250個線程
????????? Framework 2.0:每個核心25個線程
(具體數值可能因硬件和操作系統而異)。設置較高數量是為了確保當部分線程阻塞時(如等待遠程計算機響應),程序仍能繼續執行。
????????通過ThreadPool.SetMinThreads還可設置下限線程數。下限的作用更為精妙:這是一種高級優化技術,指示池管理器在達到下限前不得延遲線程分配。當存在阻塞線程時,提高最小線程數可增強并發性(參見邊欄說明)。
????????默認下限為每個處理器核心1個線程——這是實現CPU完全利用的最低要求。但在服務器環境(如IIS下的ASP.NET)中,下限通常高得多,可達50個甚至更多。
最小線程數的工作原理
????????將線程池的最小線程數提升至x,并不會立即強制創建x個線程——線程僅在需要時才會創建。實際上,這個設置是指導線程池管理器在需要時可立即創建最多x個線程。那么問題來了:為什么線程池在需要線程時會有意延遲創建呢?
原因在于防止短暫爆發的短期活動導致線程全量分配,從而突然增加應用程序的內存占用。舉例來說,假設一臺四核計算機運行的客戶端應用一次性提交40個任務:
-
若每個任務執行10毫秒的計算
-
假設工作均勻分配到四個核心
-
整個過程將在100毫秒內完成
理想情況下,我們希望40個任務正好運行在4個線程上:
-
少于4個線程無法充分利用所有核心
-
多于4個線程會浪費內存和CPU時間創建不必要的線程
而這正是線程池的實際工作方式。使線程數與核心數匹配,既能保持較小的內存占用,又不會影響性能——前提是線程被高效利用(本例即是如此)。
但當每個任務改為查詢網絡(等待半秒響應且本地CPU閑置)時,線程池的節約策略就會失效。此時創建更多線程讓所有網絡查詢并發執行反而更高效。
為此線程池準備了備用方案:如果任務隊列持續半秒未變化,就會以每半秒一個的速度新增線程,直到達到線程池容量上限。
這半秒延遲是把雙刃劍:
-
優勢:防止一次性短期活動導致程序突然多占用40MB(或更多)內存
-
劣勢:當線程阻塞時(如數據庫查詢或調用WebClient.DownloadFile)可能造成不必要延遲
因此可以通過SetMinThreads告知線程池不要延遲創建前x個線程,例如:
//(第二個參數指定分配給I/O完成端口的線程數,該機制用于異步編程模型)
ThreadPool.SetMinThreads (50, 50);
默認值為每個處理器核心1個線程。
本小節完......