.net線程池內幕

来源:http://www.cnblogs.com/newbier/archive/2016/12/17/6192882.html
-Advertisement-
Play Games

線程池的作用線程池,顧名思義,線程對象池。Task和TPL都有用到線程池,所以瞭解線程池的內幕有助於你寫出更好的程式。由於篇幅有限,在這裡我只講解以下核心概念: 線程池的大小 如何調用線程池添加任務 線程池如何執行任務 Threadpool也支持操控IOCP的線程,但在這裡我們不研究它,和task以 ...


本文通過對.NET4.5的ThreadPool源碼的分析講解揭示.NET線程池的內幕,並總結ThreadPool設計的好與不足。

線程池的作用
線程池,顧名思義,線程對象池。Task和TPL都有用到線程池,所以瞭解線程池的內幕有助於你寫出更好的程式。由於篇幅有限,在這裡我只講解以下核心概念:

  • 線程池的大小
  • 如何調用線程池添加任務
  • 線程池如何執行任務

Threadpool也支持操控IOCP的線程,但在這裡我們不研究它,涉及到task和TPL的會在其各自的博客中做詳解。

線程池的大小
不管什麼池,總有尺寸,ThreadPool也不例外。ThreadPool提供了4個方法來調整線程池的大小:

  • SetMaxThreads
  • GetMaxThreads
  • SetMinThreads
  • GetMinThreads

SetMaxThreads指定線程池最多可以有多少個線程,而GetMaxThreads自然就是獲取這個值。SetMinThreads指定線程池中最少存活的線程的數量,而GetMinThreads就是獲取這個值。
為何要設置一個最大數量和有一個最小數量呢?原來線程池的大小取決於若幹因素,如虛擬地址空間的大小等。比如你的電腦是4g記憶體,而一個線程的初始堆棧大小為1m,那麼你最多能創建4g/1m的線程(忽略操作系統本身以及其他進程記憶體分配);正因為線程有記憶體開銷,所以如果線程池的線程過多而又沒有被完全使用,那麼這就是對記憶體的一種浪費,所以限制線程池的最大數是很make sense的。
那麼最小數又是為啥?線程池就是線程的對象池,對象池的最大的用處是重用對象。為啥要重用線程,因為線程的創建與銷毀都要占用大量的cpu時間。所以在高併發狀態下,線程池由於無需創建銷毀線程節約了大量時間,提高了系統的響應能力和吞吐量。最小數可以讓你調整最小的存活線程數量來應對不同的高併發場景。

如何調用線程池添加任務
線程池主要提供了2個方法來調用:QueueUserWorkItem和UnsafeQueueUserWorkItem。
兩個方法的代碼基本一致,除了attribute不同,QueueUserWorkItem可以被partial trust的代碼調用,而UnsafeQueueUserWorkItem只能被full trust的代碼調用。

1 public static bool QueueUserWorkItem(WaitCallback callBack)
2 {
3 StackCrawlMark stackMark = StackCrawlMark.LookForMyCaller;
4 return ThreadPool.QueueUserWorkItemHelper(callBack, (object) null, ref stackMark, true);
5 }

QueueUserWorkItemHelper首先調用ThreadPool.EnsureVMInitialized()來確保CLR虛擬機初始化(VM是一個統稱,不是單指java虛擬機,也可以指CLR的execution engine),緊接著實例化ThreadPoolWorkQueue,最後調用ThreadPoolWorkQueue的Enqueue方法並傳入callback和true。

 1 [SecurityCritical]
 2 public void Enqueue(IThreadPoolWorkItem callback, bool forceGlobal)
 3 {
 4 ThreadPoolWorkQueueThreadLocals queueThreadLocals = (ThreadPoolWorkQueueThreadLocals) null;
 5 if (!forceGlobal)
 6 queueThreadLocals = ThreadPoolWorkQueueThreadLocals.threadLocals;
 7 if (this.loggingEnabled)
 8 FrameworkEventSource.Log.ThreadPoolEnqueueWorkObject((object) callback);
 9 if (queueThreadLocals != null)
10 {
11 queueThreadLocals.workStealingQueue.LocalPush(callback);
12 }
13 else
14 {
15 ThreadPoolWorkQueue.QueueSegment comparand = this.queueHead;
16 while (!comparand.TryEnqueue(callback))
17 {
18 Interlocked.CompareExchange<ThreadPoolWorkQueue.QueueSegment>(ref comparand.Next, new ThreadPoolWorkQueue.QueueSegment(), (ThreadPoolWorkQueue.QueueSegment) null);
19 for (; comparand.Next != null; comparand = this.queueHead)
20 Interlocked.CompareExchange<ThreadPoolWorkQueue.QueueSegment>(ref this.queueHead, comparand.Next, comparand);
21 }
22 }
23 this.EnsureThreadRequested();
24 }

ThreadPoolWorkQueue主要包含2個“queue”(實際是數組),一個為QueueSegment(global work queue),另一個是WorkStealingQueue(local work queue)。兩者具體的區別會在Task/TPL里講解,這裡暫不解釋。
由於forceGlobal是true,所以執行到了comparand.TryEnqueue(callback),也就是QueueSegment.TryEnqueue。comparand先從隊列的頭(queueHead)開始enqueue,如果不行就繼續往下enqueue,成功後再賦值給queueHead。
讓我們來看看QueueSegment的源代碼:

 1 public QueueSegment()
 2 {
 3 this.nodes = new IThreadPoolWorkItem[256];
 4 }
 5 
 6 public bool TryEnqueue(IThreadPoolWorkItem node)
 7 {
 8 int upper;
 9 int lower;
10 this.GetIndexes(out upper, out lower);
11 while (upper != this.nodes.Length)
12 {
13 if (this.CompareExchangeIndexes(ref upper, upper + 1, ref lower, lower))
14 {
15 Volatile.Write<IThreadPoolWorkItem>(ref this.nodes[upper], node);
16 return true;
17 }
18 }
19 return false;
20 }

這個所謂的global work queue實際上是一個IThreadPoolWorkItem的數組,而且限死256,這是為啥?難道是因為和IIS線程池(也只有256個線程)對齊?使用interlock和記憶體寫屏障volatile.write來保證nodes的正確性,比起同步鎖性能有很大的提高。最後調用EnsureThreadRequested,EnsureThreadRequested會調用QCall把請求發送至CLR,由CLR調度ThreadPool。

線程池如何執行任務
線程被調度後通過ThreadPoolWorkQueue的Dispatch方法來執行callback。

 1 internal static bool Dispatch()
 2 {
 3 ThreadPoolWorkQueue threadPoolWorkQueue = ThreadPoolGlobals.workQueue;
 4 int tickCount = Environment.TickCount;
 5 threadPoolWorkQueue.MarkThreadRequestSatisfied();
 6 threadPoolWorkQueue.loggingEnabled = FrameworkEventSource.Log.IsEnabled(EventLevel.Verbose, (EventKeywords) 18);
 7 bool flag1 = true;
 8 IThreadPoolWorkItem callback = (IThreadPoolWorkItem) null;
 9 try
10 {
11 ThreadPoolWorkQueueThreadLocals tl = threadPoolWorkQueue.EnsureCurrentThreadHasQueue();
12 while ((long) (Environment.TickCount - tickCount) < (long) ThreadPoolGlobals.tpQuantum)
13 {
14 try
15 {
16 }
17 finally
18 {
19 bool missedSteal = false;
20 threadPoolWorkQueue.Dequeue(tl, out callback, out missedSteal);
21 if (callback == null)
22 flag1 = missedSteal;
23 else
24 threadPoolWorkQueue.EnsureThreadRequested();
25 }
26 if (callback == null)
27 return true;
28 if (threadPoolWorkQueue.loggingEnabled)
29 FrameworkEventSource.Log.ThreadPoolDequeueWorkObject((object) callback);
30 if (ThreadPoolGlobals.enableWorkerTracking)
31 {
32 bool flag2 = false;
33 try
34 {
35 try
36 {
37 }
38 finally
39 {
40 ThreadPool.ReportThreadStatus(true);
41 flag2 = true;
42 }
43 callback.ExecuteWorkItem();
44 callback = (IThreadPoolWorkItem) null;
45 }
46 finally
47 {
48 if (flag2)
49 ThreadPool.ReportThreadStatus(false);
50 }
51 }
52 else
53 {
54 callback.ExecuteWorkItem();
55 callback = (IThreadPoolWorkItem) null;
56 }
57 if (!ThreadPool.NotifyWorkItemComplete())
58 return false;
59 }
60 return true;
61 }
62 catch (ThreadAbortException ex)
63 {
64 if (callback != null)
65 callback.MarkAborted(ex);
66 flag1 = false;
67 }
68 finally
69 {
70 if (flag1)
71 threadPoolWorkQueue.EnsureThreadRequested();
72 }
73 return true;
74 }

while語句判斷如果執行時間少於30ms會不斷繼續執行下一個callback。這是因為大多數機器線程切換大概在30ms,如果該線程只執行了不到30ms就在等待中斷線程切換那就太浪費CPU了,浪費可恥啊!
Dequeue負責找到需要執行的callback:

 1 public void Dequeue(ThreadPoolWorkQueueThreadLocals tl, out IThreadPoolWorkItem callback, out bool missedSteal)
 2 {
 3 callback = (IThreadPoolWorkItem) null;
 4 missedSteal = false;
 5 ThreadPoolWorkQueue.WorkStealingQueue workStealingQueue1 = tl.workStealingQueue;
 6 workStealingQueue1.LocalPop(out callback);
 7 if (callback == null)
 8 {
 9 for (ThreadPoolWorkQueue.QueueSegment comparand = this.queueTail; !comparand.TryDequeue(out callback) && comparand.Next != null && comparand.IsUsedUp(); comparand = this.queueTail)
10 Interlocked.CompareExchange<ThreadPoolWorkQueue.QueueSegment>(ref this.queueTail, comparand.Next, comparand);
11 }
12 if (callback != null)
13 return;
14 ThreadPoolWorkQueue.WorkStealingQueue[] current = ThreadPoolWorkQueue.allThreadQueues.Current;
15 int num = tl.random.Next(current.Length);
16 for (int length = current.Length; length > 0; --length)
17 {
18 ThreadPoolWorkQueue.WorkStealingQueue workStealingQueue2 = Volatile.Read<ThreadPoolWorkQueue.WorkStealingQueue>(ref current[num % current.Length]);
19 if (workStealingQueue2 != null && workStealingQueue2 != workStealingQueue1 && workStealingQueue2.TrySteal(out callback, ref missedSteal))
20 break;
21 ++num;
22 }
23 }

因為我們把callback添加到了global work queue,所以local work queue(workStealingQueue.LocalPop(out callback))找不到callback,local work queue查找callback會在task里講解。接著又去global work queue查找,先從global work queue的起始位置查找直至尾部,因此global work quque里的callback是FIFO的執行順序。

 1 public bool TryDequeue(out IThreadPoolWorkItem node)
 2 {
 3 int upper;
 4 int lower;
 5 this.GetIndexes(out upper, out lower);
 6 while (lower != upper)
 7 {
 8 // ISSUE: explicit reference operation
 9 // ISSUE: variable of a reference type
10 int& prevUpper = @upper;
11 // ISSUE: explicit reference operation
12 int newUpper = ^prevUpper;
13 // ISSUE: explicit reference operation
14 // ISSUE: variable of a reference type
15 int& prevLower = @lower;
16 // ISSUE: explicit reference operation
17 int newLower = ^prevLower + 1;
18 if (this.CompareExchangeIndexes(prevUpper, newUpper, prevLower, newLower))
19 {
20 SpinWait spinWait = new SpinWait();
21 while ((node = Volatile.Read<IThreadPoolWorkItem>(ref this.nodes[lower])) == null)
22 spinWait.SpinOnce();
23 this.nodes[lower] = (IThreadPoolWorkItem) null;
24 return true;
25 }
26 }
27 node = (IThreadPoolWorkItem) null;
28 return false;
29 }

使用自旋鎖和記憶體讀屏障來避免內核態和用戶態的切換,提高了獲取callback的性能。如果還是沒有callback,那麼就從所有的local work queue里隨機選取一個,然後在該local work queue里“偷取”一個任務(callback)。
拿到callback後執行callback.ExecuteWorkItem(),通知完成。

總結
ThreadPool提供了方法調整線程池最少活躍的線程來應對不同的併發場景。ThreadPool帶有2個work queue,一個golbal一個local。執行時先從local找任務,接著去global,最後才會去隨機選取一個local偷一個任務,其中global是FIFO的執行順序。Work queue實際上是數組,使用了大量的自旋鎖和記憶體屏障來提高性能。但是在偷取任務上,是否可以考慮得更多,隨機選擇一個local太隨意。首先要考慮偷取的隊列上必須有可執行任務;其次可以選取一個不在調度中的線程的local work queue,這樣降低了自旋鎖的可能性,加快了偷取的速度;最後,偷取的時候可以考慮像golang一樣偷取別人queue里一半的任務,因為執行完偷到的這一個任務之後,下次該線程再次被調度到還是可能沒任務可執行,還得去偷取別人的任務,這樣既浪費CPU時間,又讓任務線上程上分佈不均勻,降低了系統吞吐量!

另外,如果禁用log和ETW trace,可以使ThreadPool的性能更進一步。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1、查看進程 ps -ef | grep 關鍵字 /*關鍵字為服務名*/ netstat -unltp | grep 關鍵字 /*關鍵字為服務名或者是埠均可*/ 2、殺死進程 kill -9 進程號 /*操作需謹慎*/ 3、分頁查看文件 cat 文件名 | less 回車, 【Enter或者下鍵】 ...
  • 寫此隨筆,目的只為今後在ASP.NET MVC項目中再用到Area(區域)時作為備查。 獲取當前Area(區域)名稱的方法是: 這樣,我就可以通過下麵三個語句,分別獲取用戶當前訪問的Area、Controller和Action 當用戶訪問的是主站(根目錄)時filterContext.RouteDa ...
  • 學會使用異常 在 C# 中,程式中的運行時錯誤通過使用一種稱為“異常”的機制在程式中傳播。 異常由錯誤的代碼引發,並由能夠更正錯誤的代碼進行捕捉。 異常可由 .NET 的公共語言運行時 (CLR) 或由程式中的代碼引發。 一旦引發了一個異常,這個異常就會在調用堆棧中向上傳播,直到找到針對它的 cat ...
  • 實現微信上網頁的圖片點擊後全屏還可以可以縮放,這個功能是別人做的,可是捏點擊後屏幕直接黑屏了,圖片沒有顯示出來。這個代碼在網上搜一下,挺多類似的。 先上代碼。 在微信web 開發者工具調試,網頁上斷點調試發現圖片路徑 json 格式化了兩次!!! 最後解決的方法是沒有調用 arrayToJson() ...
  • 異常與處理 C# 語言的異常處理功能可幫助您處理程式運行時出現的任何意外或異常情況。 異常處理使用 try、catch 和 finally 關鍵字嘗試某些操作,以處理失敗情況,儘管這些操作有可能失敗,但如果您確定需要這樣做,且希望在事後清理資源,就可以嘗試這樣做。 公共語言運行時 (CLR)、.NE ...
  • 本人經常與webservice打交道,特意寫了個小工具來調用Webservice方便測試,還有待進一步完善。使用方法如下 : 填寫完webservice的wsdl地址後點擊載入,將在方法那一側列出該服務所包含的方法,選中方法後在右側列出該方法所需參數,填完參數值後點擊調用在下方顯示結果,在標題欄顯示 ...
  • 方法不能跟變數一樣當參數傳遞,怎麼辦,C#定義了委托,就可以把方法當變數一樣傳遞了,為了簡單,匿名方法傳遞,省得再聲明方法了;再簡單,lambda表達式傳遞,比匿名方法更直觀。 public delegate int delegateArithmetic(int a, int b); //委托作為參 ...
  • 裝箱是將值類型轉換為 object 類型或由此值類型實現的任何介面類型的過程。 當 CLR 對值類型進行裝箱時,會將該值包裝到 System.Object 內部,再將後者存儲在托管堆上。 取消裝箱將從對象中提取值類型。 裝箱是隱式的;拆箱是顯式的。 裝箱和拆箱的概念是類型系統 C# 統一視圖的基礎, ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...