.NET?Core?線(xiàn)程池(ThreadPool)底層原理源碼解析
簡(jiǎn)介
上文提到,創(chuàng)建線(xiàn)程在操作系統(tǒng)層面有4大無(wú)法避免的開(kāi)銷(xiāo)。因此復(fù)用線(xiàn)程明顯是一個(gè)更優(yōu)的策略,切降低了使用線(xiàn)程的門(mén)檻,提高程序員的下限。
.NET Core線(xiàn)程池日新月異,不同版本實(shí)現(xiàn)都有差別,在.NET 6之前,ThreadPool底層由C++承載。在之后由C#承載。本文以.NET 8.0.8為藍(lán)本,如有出入,請(qǐng)參考源碼.
ThreadPool結(jié)構(gòu)模型圖

眼見(jiàn)為實(shí)
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>();//全局隊(duì)列
internal readonly ConcurrentQueue<object> highPriorityWorkItems = new ConcurrentQueue<object>();//高優(yōu)先級(jí)隊(duì)列,比如Timer產(chǎn)生的定時(shí)任務(wù)
internal readonly ConcurrentQueue<object> lowPriorityWorkItems =
s_prioritizationExperiment ? new ConcurrentQueue<object>() : null!;//低優(yōu)先級(jí)隊(duì)列,比如回調(diào)
internal readonly ConcurrentQueue<object>[] _assignableWorkItemQueues =
new ConcurrentQueue<object>[s_assignableWorkItemQueueCount];//CPU 核心大于32個(gè),全局隊(duì)列會(huì)分裂為好幾個(gè),目的是降低CPU核心對(duì)全局隊(duì)列的鎖競(jìng)爭(zhēng)
}
ThreadPool生產(chǎn)者模型

眼見(jiàn)為實(shí)
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目前還是實(shí)驗(yàn)階段,CLR代碼比較偷懶,這一段代碼很不優(yōu)雅,沒(méi)有連續(xù)性。
{
EnqueueForPrioritizationExperiment(callback, forceGlobal);
}
else
#endif
{
ThreadPoolWorkQueueThreadLocals? tl;
if (!forceGlobal && (tl = ThreadPoolWorkQueueThreadLocals.threadLocals) != null)
{
tl.workStealingQueue.LocalPush(callback);//如果沒(méi)有特殊情況,默認(rèn)加入本地隊(duì)列
}
else
{
ConcurrentQueue<object> queue =
s_assignableWorkItemQueueCount > 0 && (tl = ThreadPoolWorkQueueThreadLocals.threadLocals) != null
? tl.assignedGlobalWorkItemQueue//CPU>32 加入分裂的全局隊(duì)列
: workItems;//CPU<=32 加入全局隊(duì)列
queue.Enqueue(callback);
}
}
EnsureThreadRequested();
}細(xì)心的朋友,會(huì)發(fā)現(xiàn)highPriorityWorkItems的注入判斷哪里去了?目前CLR對(duì)于高優(yōu)先級(jí)隊(duì)列只開(kāi)放給內(nèi)部,比如timer/Task使用
ThreadPool消費(fèi)者模型

眼見(jiàn)為實(shí)
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;
}什么是線(xiàn)程饑餓?
線(xiàn)程饑餓(Thread Starvation)是指線(xiàn)程長(zhǎng)時(shí)間得不到調(diào)度(時(shí)間片),從而無(wú)法完成任務(wù)。
- 線(xiàn)程被無(wú)限阻塞
當(dāng)某個(gè)線(xiàn)程獲取鎖后長(zhǎng)期不釋放,其它線(xiàn)程一直在等待 - 線(xiàn)程優(yōu)先級(jí)降低
操作系統(tǒng)鎖競(jìng)爭(zhēng)中,高優(yōu)先級(jí)線(xiàn)程,搶占低優(yōu)先級(jí)線(xiàn)程的CPU時(shí)間 - 線(xiàn)程在等待
比如線(xiàn)程Wait/Result時(shí),線(xiàn)程池資源不夠,導(dǎo)致得不到執(zhí)行
眼見(jiàn)為實(shí)
@一線(xiàn)碼農(nóng) 使用大佬的案例
http://www.dbjr.com.cn/program/3313770o1.htm
http://www.dbjr.com.cn/aspnet/3313810g7.htm
windbg sos bug依舊存在
大佬的文章中,描述sos存在bug,無(wú)法顯示線(xiàn)程堆積情況

經(jīng)實(shí)測(cè),在.net 8中依舊存在此bug

99851個(gè)積壓隊(duì)列,沒(méi)有顯示出來(lái)
ThreadPool如何改善線(xiàn)程饑餓
CLR線(xiàn)程池使用爬山算法來(lái)動(dòng)態(tài)調(diào)整線(xiàn)程池的大小來(lái)來(lái)改善線(xiàn)程饑餓的問(wèn)題。本人水平有限,放出地址,有興趣的同學(xué)可以自行研究https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Threading/PortableThreadPool.HillClimbing.cs
ThreadPool如何增加線(xiàn)程
在 PortableThreadPool 中有一個(gè)子類(lèi)叫 GateThread,它就是專(zhuān)門(mén)用來(lái)增減線(xiàn)程的類(lèi)
其底層使用一個(gè)while (true) 每隔500ms來(lái)輪詢(xún)線(xiàn)程數(shù)量是否足夠,以及一個(gè)AutoResetEvent來(lái)接收注入線(xiàn)程Event.如果不夠就新增
《CLR vir C#》 一書(shū)中,提過(guò)一句 CLR線(xiàn)程池每秒最多新增1~2個(gè)線(xiàn)程。結(jié)論的源頭就是在這里注意:是線(xiàn)程池注入線(xiàn)程每秒1~2個(gè),不是每秒只能創(chuàng)建1~2個(gè)線(xiàn)程。OS創(chuàng)建線(xiàn)程的速度塊多了。
眼見(jiàn)為實(shí)


眼見(jiàn)為實(shí)
static void Main(string[] args)
{
for (int i = 0;i<=100000;i++)
{
ThreadPool.QueueUserWorkItem((x) =>
{
Console.WriteLine($"當(dāng)前線(xiàn)程Id:{Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep(int.MaxValue);
});
}
Console.ReadLine();
}可以觀(guān)察輸出,判斷是不是每秒注入1~2個(gè)線(xiàn)程
Task
不用多說(shuō)什么了吧?
Task的底層調(diào)用模型圖

Task的底層實(shí)現(xiàn)主要取決于TaskSchedule,一般來(lái)說(shuō),除了UI線(xiàn)程外,默認(rèn)是調(diào)度到線(xiàn)程池
眼見(jiàn)為實(shí)
Task.Run(() => { { Console.WriteLine("Test"); } });其底層會(huì)自動(dòng)調(diào)用Start(),Start()底層調(diào)用的TaskShedule.QueueTask().而作為實(shí)現(xiàn)類(lèi)ThreadPoolTaskScheduler.QueueTask底層調(diào)用如下。

可以看到,默認(rèn)情況下(除非你自己實(shí)現(xiàn)一個(gè)TaskShedule抽象類(lèi)).Task的底層使用ThreadPool來(lái)管理。
有意思的是,對(duì)于長(zhǎng)任務(wù)(Long Task),直接是用一個(gè)單獨(dú)的后臺(tái)線(xiàn)程來(lái)管理,完全不參與調(diào)度。
Task對(duì)線(xiàn)程池的優(yōu)化
既然Task的底層是使用ThreadPool,而線(xiàn)程池注入速度是比較慢的。Task作為線(xiàn)程池的高度封裝,有沒(méi)有優(yōu)化呢?答案是Yes當(dāng)使用Task.Result時(shí),底層會(huì)調(diào)用InternalWaitCore(),如果Task還未完成,會(huì)調(diào)用ThreadPool.NotifyThreadBlocked()來(lái)通知ThreadPool當(dāng)前線(xiàn)程已經(jīng)被阻塞,必須馬上注入一個(gè)新線(xiàn)程來(lái)代替被阻塞的線(xiàn)程。相對(duì)每500ms來(lái)輪詢(xún)注入線(xiàn)程,該方式采用事件驅(qū)動(dòng),注入線(xiàn)程池的速度會(huì)更快。
眼見(jiàn)為實(shí)
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}: 這是耗時(shí)任務(wù)");
try
{
var content = client.GetStringAsync("https://youtube.com").Result;
Console.WriteLine(content);
}
catch (Exception)
{
throw;
}
});
}
Console.ReadLine();
}

其底層通過(guò)AutoResetEvent來(lái)觸發(fā)注入線(xiàn)程的Event消息
結(jié)論
多用Task,它更完善。對(duì)線(xiàn)程池優(yōu)化更好。沒(méi)有不使用Task的理由
到此這篇關(guān)于.NET Core 線(xiàn)程池(ThreadPool)底層原理淺談的文章就介紹到這了,更多相關(guān).NET Core 線(xiàn)程池ThreadPool內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
ASP.NET環(huán)境下為網(wǎng)站增加IP過(guò)濾功能
通過(guò)深入的交流和溝通,確認(rèn)了該發(fā)電廠(chǎng)在企業(yè)網(wǎng)站用戶(hù)訪(fǎng)問(wèn)控制方面的改進(jìn)要求2009-06-06
.net Core連接MongoDB數(shù)據(jù)庫(kù)的步驟詳解
這篇文章主要給大家介紹了關(guān)于.net Core連接MongoDB數(shù)據(jù)庫(kù)步驟的相關(guān)資料,文中將實(shí)現(xiàn)的步驟一步步介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧。2018-02-02
asp.net core webapi項(xiàng)目配置全局路由的方法示例
這篇文章主要介紹了asp.net core webapi項(xiàng)目配置全局路由的方法示例,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-09-09
Asp.net(C#)讀取數(shù)據(jù)庫(kù)并生成JS文件制作首頁(yè)圖片切換效果(附demo源碼下載)
這篇文章主要介紹了Asp.net(C#)讀取數(shù)據(jù)庫(kù)并生成JS文件制作首頁(yè)圖片切換效果的方法,涉及asp.net數(shù)據(jù)庫(kù)操作及JavaScript幻燈片生成的相關(guān)技巧,并附帶demo源碼供讀者下載參考,需要的朋友可以參考下2016-04-04
asp.net中Log4.net的工具類(lèi)helper
這篇文章介紹了asp.net中Log4.net的工具類(lèi)helper,文中通過(guò)示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-04-04
.NET中獲取Access新增記錄Id怪現(xiàn)象解決方法
寫(xiě)了一個(gè)函數(shù)獲取Access表中指定用戶(hù)Id,要求當(dāng)傳入的用戶(hù)名不存在時(shí),則在表中新增一條記錄并返回Id2012-03-03
詳解如何在.NET代碼中使用本地部署的Deepseek語(yǔ)言模型
這篇文章主要來(lái)和大家一起聊一聊怎么在?.NET?代碼中使用本地部署的?Deepseek?語(yǔ)言模型,文中的示例代碼簡(jiǎn)潔易懂,有需要的小伙伴可以了解下2025-02-02

