.NET?Core?線程池(ThreadPool)底層原理源碼解析
簡介
上文提到,創(chuàng)建線程在操作系統(tǒng)層面有4大無法避免的開銷。因此復用線程明顯是一個更優(yōu)的策略,切降低了使用線程的門檻,提高程序員的下限。
.NET Core線程池日新月異,不同版本實現(xiàn)都有差別,在.NET 6之前,ThreadPool底層由C++承載。在之后由C#承載。本文以.NET 8.0.8為藍本,如有出入,請參考源碼.
ThreadPool結(jié)構(gòu)模型圖
眼見為實
https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPoolWorkQueue.cs上源碼 and windbg
internal sealed partial class ThreadPoolWorkQueue { internal readonly ConcurrentQueue<object> workItems = new ConcurrentQueue<object>();//全局隊列 internal readonly ConcurrentQueue<object> highPriorityWorkItems = new ConcurrentQueue<object>();//高優(yōu)先級隊列,比如Timer產(chǎn)生的定時任務 internal readonly ConcurrentQueue<object> lowPriorityWorkItems = s_prioritizationExperiment ? new ConcurrentQueue<object>() : null!;//低優(yōu)先級隊列,比如回調(diào) internal readonly ConcurrentQueue<object>[] _assignableWorkItemQueues = new ConcurrentQueue<object>[s_assignableWorkItemQueueCount];//CPU 核心大于32個,全局隊列會分裂為好幾個,目的是降低CPU核心對全局隊列的鎖競爭 }
ThreadPool生產(chǎn)者模型
眼見為實
public void Enqueue(object callback, bool forceGlobal) { Debug.Assert((callback is IThreadPoolWorkItem) ^ (callback is Task)); if (_loggingEnabled && FrameworkEventSource.Log.IsEnabled()) FrameworkEventSource.Log.ThreadPoolEnqueueWorkObject(callback); #if CORECLR if (s_prioritizationExperiment)//lowPriorityWorkItems目前還是實驗階段,CLR代碼比較偷懶,這一段代碼很不優(yōu)雅,沒有連續(xù)性。 { EnqueueForPrioritizationExperiment(callback, forceGlobal); } else #endif { ThreadPoolWorkQueueThreadLocals? tl; if (!forceGlobal && (tl = ThreadPoolWorkQueueThreadLocals.threadLocals) != null) { tl.workStealingQueue.LocalPush(callback);//如果沒有特殊情況,默認加入本地隊列 } else { ConcurrentQueue<object> queue = s_assignableWorkItemQueueCount > 0 && (tl = ThreadPoolWorkQueueThreadLocals.threadLocals) != null ? tl.assignedGlobalWorkItemQueue//CPU>32 加入分裂的全局隊列 : workItems;//CPU<=32 加入全局隊列 queue.Enqueue(callback); } } EnsureThreadRequested(); }
細心的朋友,會發(fā)現(xiàn)highPriorityWorkItems的注入判斷哪里去了?目前CLR對于高優(yōu)先級隊列只開放給內(nèi)部,比如timer/Task使用
ThreadPool消費者模型
眼見為實
public object? Dequeue(ThreadPoolWorkQueueThreadLocals tl, ref bool missedSteal) { // Check for local work items object? workItem = tl.workStealingQueue.LocalPop(); if (workItem != null) { return workItem; } // Check for high-priority work items if (tl.isProcessingHighPriorityWorkItems) { if (highPriorityWorkItems.TryDequeue(out workItem)) { return workItem; } tl.isProcessingHighPriorityWorkItems = false; } else if ( _mayHaveHighPriorityWorkItems != 0 && Interlocked.CompareExchange(ref _mayHaveHighPriorityWorkItems, 0, 1) != 0 && TryStartProcessingHighPriorityWorkItemsAndDequeue(tl, out workItem)) { return workItem; } // Check for work items from the assigned global queue if (s_assignableWorkItemQueueCount > 0 && tl.assignedGlobalWorkItemQueue.TryDequeue(out workItem)) { return workItem; } // Check for work items from the global queue if (workItems.TryDequeue(out workItem)) { return workItem; } // Check for work items in other assignable global queues uint randomValue = tl.random.NextUInt32(); if (s_assignableWorkItemQueueCount > 0) { int queueIndex = tl.queueIndex; int c = s_assignableWorkItemQueueCount; int maxIndex = c - 1; for (int i = (int)(randomValue % (uint)c); c > 0; i = i < maxIndex ? i + 1 : 0, c--) { if (i != queueIndex && _assignableWorkItemQueues[i].TryDequeue(out workItem)) { return workItem; } } } #if CORECLR // Check for low-priority work items if (s_prioritizationExperiment && lowPriorityWorkItems.TryDequeue(out workItem)) { return workItem; } #endif // Try to steal from other threads' local work items { WorkStealingQueue localWsq = tl.workStealingQueue; WorkStealingQueue[] queues = WorkStealingQueueList.Queues; int c = queues.Length; Debug.Assert(c > 0, "There must at least be a queue for this thread."); int maxIndex = c - 1; for (int i = (int)(randomValue % (uint)c); c > 0; i = i < maxIndex ? i + 1 : 0, c--) { WorkStealingQueue otherQueue = queues[i]; if (otherQueue != localWsq && otherQueue.CanSteal) { workItem = otherQueue.TrySteal(ref missedSteal); if (workItem != null) { return workItem; } } } } return null; }
什么是線程饑餓?
線程饑餓(Thread Starvation)是指線程長時間得不到調(diào)度(時間片),從而無法完成任務。
- 線程被無限阻塞
當某個線程獲取鎖后長期不釋放,其它線程一直在等待 - 線程優(yōu)先級降低
操作系統(tǒng)鎖競爭中,高優(yōu)先級線程,搶占低優(yōu)先級線程的CPU時間 - 線程在等待
比如線程Wait/Result時,線程池資源不夠,導致得不到執(zhí)行
眼見為實
@一線碼農(nóng) 使用大佬的案例
http://www.dbjr.com.cn/program/3313770o1.htm
http://www.dbjr.com.cn/aspnet/3313810g7.htm
windbg sos bug依舊存在
大佬的文章中,描述sos存在bug,無法顯示線程堆積情況
經(jīng)實測,在.net 8中依舊存在此bug
99851個積壓隊列,沒有顯示出來
ThreadPool如何改善線程饑餓
CLR線程池使用爬山算法來動態(tài)調(diào)整線程池的大小來來改善線程饑餓的問題。本人水平有限,放出地址,有興趣的同學可以自行研究https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Threading/PortableThreadPool.HillClimbing.cs
ThreadPool如何增加線程
在 PortableThreadPool 中有一個子類叫 GateThread,它就是專門用來增減線程的類
其底層使用一個while (true) 每隔500ms來輪詢線程數(shù)量是否足夠,以及一個AutoResetEvent來接收注入線程Event.如果不夠就新增
《CLR vir C#》 一書中,提過一句 CLR線程池每秒最多新增1~2個線程。結(jié)論的源頭就是在這里注意:是線程池注入線程每秒1~2個,不是每秒只能創(chuàng)建1~2個線程。OS創(chuàng)建線程的速度塊多了。
眼見為實
眼見為實
static void Main(string[] args) { for (int i = 0;i<=100000;i++) { ThreadPool.QueueUserWorkItem((x) => { Console.WriteLine($"當前線程Id:{Thread.CurrentThread.ManagedThreadId}"); Thread.Sleep(int.MaxValue); }); } Console.ReadLine(); }
可以觀察輸出,判斷是不是每秒注入1~2個線程
Task
不用多說什么了吧?
Task的底層調(diào)用模型圖
Task的底層實現(xiàn)主要取決于TaskSchedule,一般來說,除了UI線程外,默認是調(diào)度到線程池
眼見為實
Task.Run(() => { { Console.WriteLine("Test"); } });
其底層會自動調(diào)用Start(),Start()底層調(diào)用的TaskShedule.QueueTask().而作為實現(xiàn)類ThreadPoolTaskScheduler.QueueTask底層調(diào)用如下。
可以看到,默認情況下(除非你自己實現(xiàn)一個TaskShedule抽象類).Task的底層使用ThreadPool來管理。
有意思的是,對于長任務(Long Task),直接是用一個單獨的后臺線程來管理,完全不參與調(diào)度。
Task對線程池的優(yōu)化
既然Task的底層是使用ThreadPool,而線程池注入速度是比較慢的。Task作為線程池的高度封裝,有沒有優(yōu)化呢?答案是Yes當使用Task.Result時,底層會調(diào)用InternalWaitCore(),如果Task還未完成,會調(diào)用ThreadPool.NotifyThreadBlocked()來通知ThreadPool當前線程已經(jīng)被阻塞,必須馬上注入一個新線程來代替被阻塞的線程。相對每500ms來輪詢注入線程,該方式采用事件驅(qū)動,注入線程池的速度會更快。
眼見為實
static void Main(string[] args) { var client = new HttpClient(); for(int i = 0; i < 100000; i++) { ThreadPool.QueueUserWorkItem(x => { Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff")} -> {x}: 這是耗時任務"); try { var content = client.GetStringAsync("https://youtube.com").Result; Console.WriteLine(content); } catch (Exception) { throw; } }); } Console.ReadLine(); }
其底層通過AutoResetEvent來觸發(fā)注入線程的Event消息
結(jié)論
多用Task,它更完善。對線程池優(yōu)化更好。沒有不使用Task的理由
到此這篇關于.NET Core 線程池(ThreadPool)底層原理淺談的文章就介紹到這了,更多相關.NET Core 線程池ThreadPool內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
ASP.NET環(huán)境下為網(wǎng)站增加IP過濾功能
通過深入的交流和溝通,確認了該發(fā)電廠在企業(yè)網(wǎng)站用戶訪問控制方面的改進要求2009-06-06.net Core連接MongoDB數(shù)據(jù)庫的步驟詳解
這篇文章主要給大家介紹了關于.net Core連接MongoDB數(shù)據(jù)庫步驟的相關資料,文中將實現(xiàn)的步驟一步步介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧。2018-02-02asp.net core webapi項目配置全局路由的方法示例
這篇文章主要介紹了asp.net core webapi項目配置全局路由的方法示例,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-09-09Asp.net(C#)讀取數(shù)據(jù)庫并生成JS文件制作首頁圖片切換效果(附demo源碼下載)
這篇文章主要介紹了Asp.net(C#)讀取數(shù)據(jù)庫并生成JS文件制作首頁圖片切換效果的方法,涉及asp.net數(shù)據(jù)庫操作及JavaScript幻燈片生成的相關技巧,并附帶demo源碼供讀者下載參考,需要的朋友可以參考下2016-04-04.NET中獲取Access新增記錄Id怪現(xiàn)象解決方法
寫了一個函數(shù)獲取Access表中指定用戶Id,要求當傳入的用戶名不存在時,則在表中新增一條記錄并返回Id2012-03-03詳解如何在.NET代碼中使用本地部署的Deepseek語言模型
這篇文章主要來和大家一起聊一聊怎么在?.NET?代碼中使用本地部署的?Deepseek?語言模型,文中的示例代碼簡潔易懂,有需要的小伙伴可以了解下2025-02-02