1. 程式人生 > >線程池(ThreadPool)

線程池(ThreadPool)

方案 分配 ati 通過 計算 工作原理 結果 中大 ntb

線程池(ThreadPool)

https://www.cnblogs.com/jonins/p/9369927.html

線程池概述
由系統維護的容納線程的容器,由CLR控制的所有AppDomain共享。線程池可用於執行任務、發送工作項、處理異步 I/O、代表其他線程等待以及處理計時器。

線程池與線程
性能:每開啟一個新的線程都要消耗內存空間及資源(默認情況下大約1 MB的內存),同時多線程情況下操作系統必須調度可運行的線程並執行上下文切換,所以太多的線程還對性能不利。而線程池其目的是為了減少開啟新線程消耗的資源(使用線程池中的空閑線程,不必再開啟新線程,以及統一管理線程(線程池中的線程執行完畢後,回歸到線程池內,等待新任務))。

時間:無論何時啟動一個線程,都需要時間(幾百毫秒),用於創建新的局部變量堆,線程池預先創建了一組可回收線程,因此可以縮短過載時間。

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

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

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

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

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 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操作建立了異步方法。例如:

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

DNS:BeginGetHostByName、BeginResolve。

Socket:BeginAccept、BeginConnect、BeginReceive等等。

WebRequest:BeginGetRequestStream、BeginGetResponse。

SqlCommand:BeginExecuteReader、BeginExecuteNonQuery等等。這可能是開發一個Web應用時最常用的異步操作了。如果需要在執行數據庫操作時得到IOCP支持,那麽需要在連接字符串中標記Asynchronous Processing為true(默認為false),否則在調用BeginXXX操作時就會拋出異常。

WebServcie:例如.NET 2.0或WCF生成的Web Service Proxy中的BeginXXX方法、WCF中ClientBase的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.安全設置(壓縮棧、Thread的Principal屬性、winodws身份)。

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

3.邏輯調用上下文數據(System.Runtime.Remoting.Messaging.CallContext的LogicalGetData和LogicalSetData方法)。

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

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

默認情況下,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,用於表示任意異步操作。 TAP之後再討論。關於三種異步操作詳細說明請戳:這裏

2.BackgroundWorker
BackgroundWorker本質上是使用線程池內工作者線程,不過這個類已經多余了(了解即可)。在BackgroundWorker的DoWork屬性追加自定義方法,通過RunWorkerAsync將自定義方法追加進池化線程內處理。

DoWork本質上是一個事件(event)。委托類型限制為無返回值且參數有兩個分別為Object和DoWorkEventArgs類型。

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/

...

線程池(ThreadPool)