.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.dhdzp.com/program/3313770o1.htm
http://www.dhdzp.com/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的理由
到此這篇關(guān)于.NET Core 線程池(ThreadPool)底層原理淺談的文章就介紹到這了,更多相關(guān).NET Core 線程池ThreadPool內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
WPF中button按鈕同時點擊多次觸發(fā)click解決方法
這篇文章主要為大家詳細介紹了WPF中button按鈕同時點擊多次觸發(fā)click的解決方法,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-04-04
Repeater控件分別綁定數(shù)組和ArrayList實現(xiàn)思路
在后臺用DataSource綁上數(shù)據(jù)源(數(shù)組或ArrayList)在調(diào)用DataBind()方法,在前臺調(diào)用%# GetDataItem()%,感興趣的朋友可以了解下啊,望本文可以鞏固你的數(shù)據(jù)綁定知識2013-01-01
Asp.net回調(diào)技術(shù)Callback學習筆記
這篇文章主要記錄了Asp.net回調(diào)技術(shù)Callback的一些知識,感興趣的朋友可以參考下2014-08-08
GridView分頁的實現(xiàn)以及自定義分頁樣式功能實例
本文為大家詳細介紹下GridView實現(xiàn)分頁并自定義的分頁樣式,具體示例代碼如下,有想學習的朋友可以參考下哈,希望對大家有所幫助2013-07-07
Asp.Net MVC4通過id更新表單內(nèi)容的思路詳解
一個表單一旦創(chuàng)建完,其中大部分的字段便不可再編輯。只能編輯其中部分字段。下面通過本文給大家分享Asp.Net MVC4通過id更新表單內(nèi)容的思路詳解,需要的朋友參考下吧2017-07-07
關(guān)于DDD:管理"工作單元實例"的兩種模式的使用方法
本篇文章介紹了,關(guān)于DDD:管理"工作單元實例"的兩種模式的使用方法。需要的朋友參考下2013-04-04
ASP.NET?Core項目中調(diào)用WebService的方法
這篇文章介紹了ASP.NET?Core項目中調(diào)用WebService的方法,文中通過示例代碼介紹的非常詳細。對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-03-03
在Repeater控件中通過Eval的方式綁定Style樣式代碼
這篇文章主要介紹了如何在Repeater控件中通過Eval的方式綁定Style樣式,需要的朋友可以參考下2014-04-04

