線程池(ThreadPool)

線程池概述

由系統維護的容納線程的容器,由CLR控制的全部AppDomain共享。線程池可用於執行任務、發送工做項、處理異步 I/O、表明其餘線程等待以及處理計時器。html

 

線程池與線程

性能:每開啓一個新的線程都要消耗內存空間及資源(默認狀況下大約1 MB的內存),同時多線程狀況下操做系統必須調度可運行的線程並執行上下文切換,因此太多的線程還對性能不利。而線程池其目的是爲了減小開啓新線程消耗的資源(使用線程池中的空閒線程,沒必要再開啓新線程,以及統一管理線程(線程池中的線程執行完畢後,迴歸到線程池內,等待新任務))。程序員

時間:不管什麼時候啓動一個線程,都須要時間(幾百毫秒),用於建立新的局部變量堆,線程池預先建立了一組可回收線程,所以能夠縮短過載時間。web

線程池缺點:線程池的性能損耗優於線程(經過共享和回收線程的方式實現),可是:算法

1.線程池不支持線程的取消、完成、失敗通知等交互性操做。數據庫

2.線程池不支持線程執行的前後次序排序。編程

3.不能設置池化線程(線程池內的線程)的Name,會增長代碼調試難度。windows

4.池化線程一般都是後臺線程,優先級爲ThreadPriority.Normal。設計模式

5.池化線程阻塞會影響性能(阻塞會使CLR錯誤地認爲它佔用了大量CPU。CLR可以檢測或補償(往池中注入更多線程),可是這可能使線程池受到後續超負荷的印象。Task解決了這個問題)。數組

6.線程池使用的是全局隊列,全局隊列中的線程依舊會存在競爭共享資源的狀況,從而影響性能(Task解決了這個問題方案是使用本地隊列)。緩存

 

線程池工做原理

CLR初始化時,線程池中是沒有線程的。在內部,線程池維護了一個操做請求隊列。應用程序執行一個異步操做時,會將一個記錄項追加到線程池的隊列中。線程池的代碼從這個隊列中讀取記錄將這個記錄項派發給一個線程池線程。若是線程池沒有線程,就建立一個新線程。當線程池線程完成工做後,線程不會被銷燬,相反線程會返回線程池,在那裏進入空閒狀態,等待響應另外一個請求,因爲線程不銷燬自身,因此再也不產生額外的性能損耗。

程序向線程池發送多條請求,線程池嘗試只用這一個線程來服務全部請求,當請求速度超過線程池線程處理任務速度,就會建立額外線程,因此線程池沒必要建立大量線程。

若是中止向線程池發送任務,池中大量空閒線程將在一段時間後本身醒來終止本身以釋放資源(CLR不一樣版本對這個事件定義不一)。

 

工做者線程&I/O線程

線程池容許線程在多個CPU內核上調度任務,使多個線程能併發工做,從而高效率的使用系統資源,提高程序的吞吐性。

CLR線程池分爲工做者線程與I/O線程兩種:

工做者線程(workerThreads):負責管理CLR內部對象的運做,提供」運算能力「,因此一般用於計算密集(compute-bound)性操做。

I/O線程(completionPortThreads):主要用於與外部系統交換信息(如讀取一個文件)和分發IOCP中的回調。

注意:線程池會預先緩存一些工做者線程由於建立新線程的代價比較昂貴。

 

IO完成端口(IOCP)

IO完成端口(IOCP、I/O completion port):IOCP是一個異步I/O的API(能夠看做一個消息隊列),提供了處理多個異步I/O請求的線程模型,它能夠高效地將I/O事件通知給應用程序。IOCP由CLR內部維護,當異步IO請求完成時,設備驅動就會生成一個I/O請求包(IRP、I/O Request Packet),並排隊(先入先出)放入完成端口。以後會由I/O線程提取完成IRP並調用以前的委託。

I/O線程&IOCP&IRP:

當執行I/O操做時(同步I/O操做 and 異步I/O操做),都會調用Windows的API方法將當前的線程從用戶態轉變成內核態,同時生成並初始化一個I/O請求包,請求包中包含一個文件句柄,一個偏移量和一個Byte[]數組。I/O操做向內核傳遞請求包,根據這個請求包,windows內核確認這個I/O操做對應的是哪一個硬件設備。這些I/O操做會進入設備本身的處理隊列中,該隊列由這個設備的驅動程序維護。

若是是同步I/O操做,那麼在硬件設備操做I/O的時候,發出I/O請求的線程因爲」等待「(無人任務處理)被Windows變成睡眠狀態,當硬件設備完成操做後,再喚醒這個線程。因此性能不高,若是請求數不少,那麼休眠的線程數也不少,浪費大量資源。

若是是異步I/O操做(在.Net中,異步的I/O操做都是以Beginxxx形式開始,內部實現爲ThreadPool.BindHandle,須要傳入一個委託,該委託會隨着IRP一路傳遞到設備的驅動程序),該方法在Windows把I/O請求包發送到設備的處理隊列後就會返回。同時,CLR會分配一個可用的線程用於繼續執行接下來的任務,當任務完成後,經過IOCP提醒CLR它工做已經完成,當接收到通知後將該委託再放到CLR線程池隊列中由I\O線程進行回調。

因此:大多數狀況下,發人員使用工做者線程,I/O線程由CLR調用(開發者並不會直接使用)

 

基礎線程池&工做者線程(ThreadPool)

.NET中使用線程池用到ThreadPool類,ThreadPool是一個靜態類,定義於System.Threading命名空間,自.NET 1.1起引入。

調用方法QueueUserWorkItem能夠將一個異步的計算限制操做放到線程池的隊列中,這個方法向線程池的隊列添加一個工做項以及可選的狀態數據。
工做項:由callBack參數標識的一個方法,該方法由線程池線程調用。可向方法傳遞一個state實參(多於一個參數則須要封裝爲實體類)。

1  public static bool QueueUserWorkItem(WaitCallback callBack);
2  public static bool QueueUserWorkItem(WaitCallback callBack, object state);

 下面是經過QueueUserWorkItem啓動工做者線程的示例:

 1     class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             //方式一
 6             {
 7                 ThreadPool.QueueUserWorkItem(n => Test("Test-ok"));
 8             }
 9             //方式二
10             {
11                 WaitCallback waitCallback = new WaitCallback(Test);
12                 ThreadPool.QueueUserWorkItem(n => waitCallback("WaitCallback"));//二者效果相同 ThreadPool.QueueUserWorkItem(waitCallback,"Test-ok"); 13             }
14             //方式三
15             {
16                 ParameterizedThreadStart parameterizedThreadStart = new ParameterizedThreadStart(Test);
17                 ThreadPool.QueueUserWorkItem(n => parameterizedThreadStart("ParameterizedThreadStart"));
18             }
19             //方式四
20             {
21                 TimerCallback timerCallback = new TimerCallback(Test);
22                 ThreadPool.QueueUserWorkItem(n => timerCallback("TimerCallback"));
23             }
24             //方式五
25             {
26                 Action<object> action = Test;
27                 ThreadPool.QueueUserWorkItem(n => Test("Action"));
28             }
29             //方式六
30             ThreadPool.QueueUserWorkItem((o) =>
31             {
32                 var msg = "lambda";
33                 Console.WriteLine("執行方法:{0}", msg);
34             });
35             
36             ......
37 
38             Console.ReadKey();
39         }
40         static void Test(object o)
41         {
42             Console.WriteLine("執行方法:{0}", o);
43         }
44         /*
45          * 做者:Jonins
46          * 出處:http://www.cnblogs.com/jonins/
47          */
48     }

執行結果以下:

以上是使用線程池的幾種寫法,WaitCallback本質上是一個參數爲Object類型無返回值的委託

1  public delegate void WaitCallback(object state);

因此符合要求的類型均可以如上述示例代碼做爲參數進行傳遞。

 

線程池經常使用方法

ThreadPool經常使用的幾個方法以下

方法 說明
QueueUserWorkItem 啓動線程池裏的一個線程(工做者線程)
GetMinThreads 檢索線程池在新請求預測中可以按需建立的線程的最小數量。
GetMaxThreads 最多可用線程數,全部大於此數目的請求將保持排隊狀態,直到線程池線程由空閒。
GetAvailableThreads 剩餘空閒線程數。
SetMaxThreads 設置線程池中的最大線程數(請求數超過此值則進入隊列)。
SetMinThreads 設置線程池最少須要保留的線程數。

 示例代碼:

 1         static void Main(string[] args)
 2         {
 3             //聲明變量 (工做者線程計數  Io完成端口計數)
 4             int workerThreadsCount, completionPortThreadsCount;
 5             {
 6                 ThreadPool.GetMinThreads(out workerThreadsCount, out completionPortThreadsCount);
 7                 Console.WriteLine("最小工做線程數:{0},最小IO線程數{1}", workerThreadsCount, completionPortThreadsCount);
 8             }
 9             {
10                 ThreadPool.GetMaxThreads(out workerThreadsCount, out completionPortThreadsCount);
11                 Console.WriteLine("最大工做線程數:{0},最大IO線程數{1}", workerThreadsCount, completionPortThreadsCount);
12             }
13             ThreadPool.QueueUserWorkItem((o) => {
14                 Console.WriteLine("佔用1個池化線程");
15             });
16             {
17                 ThreadPool.GetAvailableThreads(out workerThreadsCount, out completionPortThreadsCount);
18                 Console.WriteLine("剩餘工做線程數:{0},剩餘IO線程數{1}", workerThreadsCount, completionPortThreadsCount);
19             }
20             Console.ReadKey();
21         }

 執行的結果:

注意

1.線程有內存開銷,因此線程池內的線程過多而沒有徹底利用是對內存的一種浪費,因此須要對線程池限制最小線程數量。 

2.線程池最大線程數是線程池最多可建立線程數,實際狀況是線程池內的線程數是按需建立。

 

I/O線程

I\O線程是.NET專爲訪問外部資源所引入的一種線程,訪問外部資源時爲了防止主線程長期處於阻塞狀態,.NET爲多個I/O操做創建了異步方法。例如:

FileStreamBeginReadBeginWrite。調用BeginRead/BeginWrite時會發起一個異步操做,可是隻有在建立FileStream時傳入FileOptions.Asynchronous參數才能獲取真正的IOCP支持,不然BeginXXX方法將會使用默認定義在Stream基類上的實現。Stream基類中BeginXXX方法會使用委託的BeginInvoke方法來發起異步調用——這會使用一個額外的線程來執行任務(並不受IOCP支持,可能額外增長性能損耗)。

DNSBeginGetHostByNameBeginResolve

SocketBeginAcceptBeginConnectBeginReceive等等。

WebRequestBeginGetRequestStreamBeginGetResponse

SqlCommandBeginExecuteReaderBeginExecuteNonQuery等等。這多是開發一個Web應用時最經常使用的異步操做了。若是須要在執行數據庫操做時獲得IOCP支持,那麼須要在鏈接字符串中標記Asynchronous Processing爲true(默認爲false),不然在調用BeginXXX操做時就會拋出異常。

WebServcie:例如.NET 2.0或WCF生成的Web Service Proxy中的BeginXXX方法、WCF中ClientBase<TChannel>的InvokeAsync方法。

這些異步方法的使用方式都比較相似,都是以Beginxxx開始(內部實現爲ThreadPool.BindHandle),以Endxxx結束。

注意

1.對於APM而言必須使用Endxxx結束異步,不然可能會形成資源泄露。

2.委託的BeginInvoke方法並不能得到IOCP支持。

3.IOCP不佔用線程。

下面是使用WebRequest的一個示例調用異步API佔用I/O線程:

 1     class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             int workerThreadsCount, completionPortThreadsCount;
 6             ThreadPool.GetAvailableThreads(out workerThreadsCount, out completionPortThreadsCount);
 7             Console.WriteLine("剩餘工做線程數:{0},剩餘IO線程數{1}", workerThreadsCount, completionPortThreadsCount);
 8             //調用WebRequest類的異步API佔用IO線程
 9             {
10                 WebRequest webRequest = HttpWebRequest.Create("http://www.cnblogs.com/jonins");
11                 webRequest.BeginGetResponse(result =>
12                 {
13                     Thread.Sleep(2000);
14                     Console.WriteLine(Thread.CurrentThread.ManagedThreadId + ":執行最終響應的回調");
15                     WebResponse webResponse = webRequest.EndGetResponse(result);
16                 }, null);
17             }
18             Thread.Sleep(1000);
19             ThreadPool.GetAvailableThreads(out workerThreadsCount, out completionPortThreadsCount);
20             Console.WriteLine("剩餘工做線程數:{0},剩餘IO線程數{1}", workerThreadsCount, completionPortThreadsCount);
21             Console.ReadKey();
22         }
23     }

執行結果以下:

有關I/O線程的內容點到此爲止,感受更可能是I/O操做、文件等方面的知識點跟線程池瓜葛很少,想了解更多戳:這裏

 

執行上下文

每一個線程都關聯了一個執行上下文數據結構,執行上下文(execution context)包括:

1.安全設置(壓縮棧、ThreadPrincipal屬性、winodws身份)。

2.宿主設置(System.Threading.HostExecutionContextManager)。

3.邏輯調用上下文數據(System.Runtime.Remoting.Messaging.CallContextLogicalGetDataLogicalSetData方法)。

線程執行它的代碼時,一些操做會受到線程執行上下文限制,尤爲是安全設置的影響。

當主線程使用輔助線程執行任務時,前者的執行上下文「流向」(複製到)輔助線程,這確保了輔助線程執行的任何操做使用的是相同的安全設置和宿主設置。

默認狀況下,CLR自動形成初始化線程的執行上下文「流向」任何輔助線程。但這會對性能形成影響。執行上下包含的大量信息採集並複製到輔助線程要耗費時間,若是輔助線程又採用了更多的輔助線程還必須建立和初始化更多的執行上下文數據結構。

System.Threading命名空間的ExecutionContext類,它容許控制線程執行上下文的流動:

 1     class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             //將一些數據放到主函數線程的邏輯調用上下文中
 6             CallContext.LogicalSetData("Action", "Jonins");
 7             //初始化要由另外一個線程作的一些事情,線程池線程能訪問邏輯上下文數據
 8             ThreadPool.QueueUserWorkItem(state => Console.WriteLine("輔助線程A:" + Thread.CurrentThread.ManagedThreadId + ";Action={0}", CallContext.LogicalGetData("Action")));
 9             //如今阻止主線程執行上下文流動
10             ExecutionContext.SuppressFlow();
11             //初始化要由另外一個線程作的一些事情,線程池線程能訪問邏輯上下文數據
12             ThreadPool.QueueUserWorkItem(state => Console.WriteLine("輔助線程B:" + Thread.CurrentThread.ManagedThreadId + ";Action={0}", CallContext.LogicalGetData("Action")));
13             //恢復主線程的執行上下文流動,以免使用更多的線程池線程
14             ExecutionContext.RestoreFlow();
15             Console.ReadKey();
16         }
17     }

結果以下:

ExecutionContext類阻止上下文流動以提高程序的性能,對於服務器應用程序,性能的提高可能很是顯著。可是客戶端應用程序的性能提高不了多少。另外,因爲SuppressFlow方法用[SecurityCritical]特性標記,因此某些客戶端如Silverlight中是沒法調用的。

注意

1.輔助線程在不須要或者不訪問上下文信息時,應阻止執行上下文的流動。

2.執行上下文流動的相關知識,在使用Task對象以及發起異步I/O操做時,一樣有用。

 

三種異步模式(掃盲)&BackgroundWorker 

1.APM&EAP&TAP

.NET支持三種異步編程模式分別爲APM、EAP和TAP:

1.基於事件的異步編程設計模式 (EAP,Event-based Asynchronous Pattern)

EAP的編程模式的代碼命名有如下特色: 

1.有一個或多個名爲 「[XXX]Async」 的方法。這些方法可能會建立同步版本的鏡像,這些同步版本會在當前線程上執行相同的操做。
2.該類還可能有一個 「[XXX]Completed」 事件,監聽異步方法的結果。
3.它可能會有一個 「[XXX]AsyncCancel」(或只是 CancelAsync)方法,用於取消正在進行的異步操做。

2.異步編程模型(APM,Asynchronous Programming Model)

APM的編程模式的代碼命名有如下特色:

1.使用 IAsyncResult 設計模式的異步操做是經過名爲[BeginXXX] 和 [EndXXX] 的兩個方法來實現的,這兩個方法分別開始和結束異步操做 操做名稱。例如,FileStream 類提供 BeginRead 和 EndRead 方法來從文件異步讀取字節。

2.在調用 [BeginXXX] 後,應用程序能夠繼續在調用線程上執行指令,同時異步操做在另外一個線程上執行。 每次調用 [BeginXXX] 時,應用程序還應調用 [EndXXX] 來獲取操做的結果。


3.基於任務的編程模型(TAP,Task-based Asynchronous Pattern)

基於 System.Threading.Tasks 命名空間的 Task 和 Task<TResult>,用於表示任意異步操做。 TAP以後再討論。關於三種異步操做詳細說明請戳:這裏 

2.BackgroundWorker 

BackgroundWorker本質上是使用線程池內工做者線程,不過這個類已經多餘了(瞭解便可)。在BackgroundWorkerDoWork屬性追加自定義方法,經過RunWorkerAsync將自定義方法追加進池化線程內處理。

DoWork本質上是一個事件(event)。委託類型限制爲無返回值且參數有兩個分別爲ObjectDoWorkEventArgs類型。

1 public event DoWorkEventHandler DoWork;
2 
3 public delegate void DoWorkEventHandler(object sender, DoWorkEventArgs e);

示例以下:

 1     class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             int workerThreadsCount, completionPortThreadsCount;
 6             ThreadPool.GetAvailableThreads(out workerThreadsCount, out completionPortThreadsCount);
 7             Console.WriteLine("剩餘工做線程數:{0},剩餘IO線程數{1}", workerThreadsCount, completionPortThreadsCount);
 8             {
 9                 BackgroundWorker backgroundWorker = new BackgroundWorker();
10                 backgroundWorker.DoWork += DoWork;
11                 backgroundWorker.RunWorkerAsync();
12             }
13             Thread.Sleep(1000);
14             ThreadPool.GetAvailableThreads(out workerThreadsCount, out completionPortThreadsCount);
15             Console.WriteLine("剩餘工做線程數:{0},剩餘IO線程數{1}", workerThreadsCount, completionPortThreadsCount);
16             Console.ReadKey();
17         }
18         private static void DoWork(object sender, DoWorkEventArgs e)
19         {
20             Thread.Sleep(2000);
21             Console.WriteLine("demo-ok");
22         }
23     }

內部佔用線程內線程,結果以下:

 

結語

程序員使用線程池更多的是使用線程池內的工做者線程進行邏輯編碼。

相對於單獨操做線程(Thread線程池(ThreadPool可以保證計算密集做業的臨時過載不會引發CPU超負荷(激活的線程數量多於CPU內核數量,系統必須按時間片執行線程調度)。

超負荷會影響性能,由於劃分時間片須要大量的上下文切換開銷,而且使CPU緩存失效,而這些是處理器實現高效的必要調度。

CLR可以將任務進行排序,而且控制任務啓動數量,從而避免線程池超負荷。CLR首先運行與硬件內核數量同樣多的併發任務,而後經過登山算法調整併發數量,保證程序切合最優性能曲線。

 

參考文獻

CLR via C#(第4版) Jeffrey Richter

C#高級編程(第10版) C# 6 & .NET Core 1.0   Christian Nagel  

果殼中的C# C#5.0權威指南  Joseph Albahari

http://www.cnblogs.com/dctit/

http://www.cnblogs.com/kissdodog/         

http://www.cnblogs.com/JeffreyZhao/

...

相關文章
相關標籤/搜索