在應用程序中有許多復雜的任務,對於這些任務可能需要使用一個或多個工作線程或I/O線程來協作處理,比如:定時任務、數據庫數據操作、web服務、文件的處理等。這些任務可能會非常耗費時間,為了是用戶界面能及時響應,就會啟用一個其他線程來並行處理任務。線程的創建和銷毀操作是非常昂貴的,過多的線程會帶來內存資源的消耗以及操作系統調度可執行線程並執行上下文切換導致的時間消耗,所以過多線程會損壞應用程序的性能。如果創建過的線程能反復使用就能解決上面的一些問題,因此,CLR使用了線程池來管理線程。
1. 線程池基礎
每個CLR 擁有一個線程池,這個線程池由CLR控制的APPDomain 共享。在線程池內部,它自己維護着一個操作請求隊列,應用程序需要執行某個任務時,就需要調用線程池的一個方法(通常是QueueUserWorkItem 方法)將任務添加到線程池工作項中,線程池就會將任務分派給一個線程池線程處理,如果線程池中沒有線程,就會創建一個線程來處理這個任務。當任務執行完成以后,這個線程會回到線程池中處於空閑狀態,等待下一個執行任務。由於線程不會銷毀,所以使用線程池線程在執行任務的速度上會更快。
如果線程池中的任務過多超過了現有線程的處理能力時,線程池就會根據需要在創建更多的線程。由於每個線程都要占用一定的內存資源,所以當線程池空閑線程(長時間不執行任務的線程)過多時,線程池中線程會自動醒來銷毀多余的空閑線程,以減少資源的使用。
在線程池內部,所有線程都是后台線程並且調度優先級都為普通(ThreadPriority.Normal),這些線程分為工作者或I/O線程,當線程池線程執行的任務是一個復雜的計算任務時,使用的就是工作者線程。如果執行的任務與I/O相關,就會使用I/O線程。
2. 使用ThreadPool 類執行異步任務
ThreadPool 類是一個靜態類型類,使用ThreadPool 類執行異步時通常調用ThreadPool 的 QueueUserWorkItem 方法,這個方法有一個重載版本,如下:
public static bool QueueUserWorkItem(WaitCallback callBack); public static bool QueueUserWorkItem(WaitCallback callBack, object state);
QueueUserWorkItem 方法接受一個WaitCallback 類型的委托作為回調方法以及可以選擇傳遞一個線程池線程執行回調方法時所需要的數據對象。
WaitCallback 委托類型的定義如下:
public delegate void WaitCallback(object state);線程池的QueueUserWorkItem方法在調用以后會立即返回,所傳遞的回調方法會有以后線程池線程執行。使用線程池線程執行異步任務代碼如下:
1: static void Main(string[] args)
2: {
3: Console.WriteLine("主線程開始執行任務。線程ID:{0}",Thread.CurrentThread.ManagedThreadId);
4:
5: //使用 ThreadPool.QueueUserWorkItem 方法將一個異步任務添加到線程池任務隊列中,
6: //可以為線程池線程執行方法時傳遞一個數據對象,
7: //如果不需要傳遞數據可以使用QueueUserWorkItem只有WaitCallback一個參數類型的版本,
8: //或傳遞null
9: ThreadPool.QueueUserWorkItem(state => {
10: Console.WriteLine("線程池線程開始執行異步任務。線程ID:{0}",Thread.CurrentThread.ManagedThreadId);
11: for (int i = 0; i < 10; i++)
12: {
13: Console.WriteLine(i);
14: }
15:
16: },null);
17:
18: Console.WriteLine("主線程執行其他任務。線程ID:{0}", Thread.CurrentThread.ManagedThreadId);
19: //使調用線程睡眠2000毫秒,等待線程池線程執行完成。
20: Thread.Sleep(2000);
21:
22: Console.WriteLine("主線程繼續執行任務。線程ID:{0}", Thread.CurrentThread.ManagedThreadId);
23: }
3.線程池對線程的管理
CLR 允許開發人員設置線程池需要創建的最大和最小工作者線程和I/O線程,但是最好不要設置線程池的線程數,CLR對線程池的工作者線程和I/O線程的最大線程數設置了1000的默認值。我們可以使用線程池的如下方法對線程池線程數進行設置和獲取以及獲得最大線程池線程數和當前運行線程數之間的差值:
//設置可以同時處於活動狀態的線程池的請求數目。 // 所有大於此數目的請求將保持排隊狀態,直到線程池線程變為可用。 public static bool SetMaxThreads(int workerThreads, int completionPortThreads); // 發出新的請求時,在切換到管理線程創建和銷毀的算法之前設置線程池按需創建的線程的最小數量。 public static bool SetMinThreads(int workerThreads, int completionPortThreads); //檢索可以同時處於活動狀態的線程池請求的數目。 所有大於此數目的請求將保持排隊狀態,直到線程池線程變為可用。 public static void GetMaxThreads(out int workerThreads, out int completionPortThreads); //發出新的請求時,在切換到管理線程創建和銷毀的算法之前檢索線程池按需創建的線程的最小數量。 public static void GetMinThreads(out int workerThreads, out int completionPortThreads);
//獲得最大線程池線程數和當前運行線程數之間的差值。 public static void GetAvailableThreads(out int workerThreads, out int completionPortThreads);
示例代碼:
1: static void SetThreadNumber() {
2: int worker, io;
3: //獲得線程池默認最大線程數
4: ThreadPool.GetMaxThreads(out worker,out io);
5: Console.WriteLine("1、CLR線程池默認最大線程數據,工作者線程數:{0},IO線程數:{1}",worker,io);
6: //設置線程池最大線程數
7: ThreadPool.SetMaxThreads(100,100);
8: ThreadPool.QueueUserWorkItem(state =>
9: {
10: Thread.Sleep(2000);
11: Console.WriteLine("4、線程池線程開始執行異步任務。線程ID:{0}", Thread.CurrentThread.ManagedThreadId);
12:
13: });
14: Console.WriteLine("2、自定義設置線程池默認最大線程數據后,工作者線程數:{0},IO線程數:{1}", worker, io);
15: //獲得最大線程池線程數和當前運行線程數之間的差值。
16: ThreadPool.GetAvailableThreads(out worker,out io);
17: Console.WriteLine("3、獲得最大線程池線程數和當前運行線程數之間的差值,工作者線程:{0},IO線程:{1}",worker,io);
18: Console.Read();
19: }
線程池的工作者線程是許多異步計算任務所使用的線程,在線程池內部,工作者線程采用先入先出的算法將工作項從線程的全局隊列中取出工作項並執行任務。在同一時刻可能有多個工作者線程從全局隊列中取出工作項,因此所有工作者線程都會競爭一個線程同步鎖,以保證兩個或更多工作者線程不會在同一時刻取出工作項。線程池工作者線程數據結構如下圖:
線程工作者線程數據結構圖
線程池在創建工作者線程時,默認會創建ThreadPool.SetMinThreads 方法鎖設置的值。如果沒有設置這個值,就會創建與應用程序進程允許的使用的CPU數相同的工作者線程,這些工作者線程監視線程池任務的執行情況,得以動態的創建更多或銷毀空閑線程。
4.線程執行上下文的流動
每個線程都有一個執行上下文,執行上下文包涵了安全設置、宿主設置和邏輯調用上下文數據,CLR默認是把初始線程的執行上下文數據向輔助線程流動,初始線程在收集和復制執行上下文數據並傳遞到輔助線程時會帶來性能的損失。如果不需要這些執行上下文數據可以使用 System.Threading.ExecutionContext 類阻止執行上下文數據的流轉。常用方法如下:
// 指示當前是否取消了執行上下文的流動。 public static bool IsFlowSuppressed(); // 恢復執行上下文在異步線程之間的流動。 public static void RestoreFlow(); //取消執行上下文在異步線程之間的流動。 public static AsyncFlowControl SuppressFlow();
執行上下文流轉控制示例代碼:
1: static void ExecContext() {
2: //把對象存儲在調用邏輯上下文中
3: System.Runtime.Remoting.Messaging.CallContext.LogicalSetData("Key","這是主線程的執行上下文數據");
4: Console.WriteLine("主線程執行上下文數據:{0}",System.Runtime.Remoting.Messaging.CallContext.LogicalGetData("Key"));
5: //阻止主線程執行上下文數據的傳遞
6: ExecutionContext.SuppressFlow();
7: Console.WriteLine("調用SuppressFlow方法以后上下文傳遞流轉是否被阻止:{0}", ExecutionContext.IsFlowSuppressed().ToString());
8:
9: ThreadPool.QueueUserWorkItem(state => {
10: Console.WriteLine("執行上下文數據被阻止傳遞:{0}",
11: System.Runtime.Remoting.Messaging.CallContext.LogicalGetData("Key"));
12: });
13:
14: //恢復主線程執行上下文數據的傳遞
15: ExecutionContext.RestoreFlow();
16: Console.WriteLine("調用RestoreFlow方法以后上下文傳遞流轉是否被阻止:{0}", ExecutionContext.IsFlowSuppressed().ToString());
17:
18: ThreadPool.QueueUserWorkItem(state =>
19: {
20: Console.WriteLine("執行上下文數據沒有被阻止傳遞:{0}",
21: System.Runtime.Remoting.Messaging.CallContext.LogicalGetData("Key"));
22: });
23: Console.Read();
24: }

本文簡單介紹了CLR 中線程池的基礎和使用以及線程池對線程的管理,如果需要構建可伸縮性、高性能、高並發的應用程序,線程池是一種很好的進行這些處理的技術。