简介

上文提到,创建线程在操作系统层面有4大无法避免的开销。因此复用线程明显是一个更优的策略,切降低了使用线程的门槛,提高程序员的下限。

.NET Core线程池日新月异,不同版本实现都有差别,在.NET 6之前,ThreadPool底层由C++承载。在之后由C#承载。本文以.NET 8.0.8为蓝本,如有出入,请参考源码.

ThreadPool结构模型图

眼见为实

https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPoolWorkQueue.cs

上源码

internal sealed partial class ThreadPoolWorkQueue
{
internal readonly ConcurrentQueue<object> workItems = new ConcurrentQueue<object>();//全局队列
internal readonly ConcurrentQueue<object> highPriorityWorkItems = new ConcurrentQueue<object>();//高优先级队列,比如Timer产生的定时任务
internal readonly ConcurrentQueue<object> lowPriorityWorkItems =
s_prioritizationExperiment ? new ConcurrentQueue<object>() : null!;//低优先级队列,比如回调
internal readonly ConcurrentQueue<object>[] _assignableWorkItemQueues =
new ConcurrentQueue<object>[s_assignableWorkItemQueueCount];//CPU 核心大于32个,全局队列会分裂为好几个,目的是降低CPU核心对全局队列的锁竞争
}

ThreadPool生产者模型

眼见为实

        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代码比较偷懒,这一段代码很不优雅,没有连续性。
{
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();
}

细心的朋友,会发现highPriorityWorkItems的注入判断哪里去了?目前CLR对于高优先级队列只开放给内部,比如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)是指线程长时间得不到调度(时间片),从而无法完成任务。

  1. 线程被无限阻塞

    当某个线程获取锁后长期不释放,其它线程一直在等待
  2. 线程优先级降低

    操作系统锁竞争中,高优先级线程,抢占低优先级线程的CPU时间
  3. 线程在等待

    比如线程Wait/Result时,线程池资源不够,导致得不到执行

眼见为实

@一线码农 使用大佬的案例

https://www.cnblogs.com/huangxincheng/p/15069457.html

https://www.cnblogs.com/huangxincheng/p/17831401.html

ThreadPool如何改善线程饥饿

CLR线程池使用爬山算法来动态调整线程池的大小来来改善线程饥饿的问题。

本人水平有限,放出地址,有兴趣的同学可以自行研究

https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Threading/PortableThreadPool.HillClimbing.cs

ThreadPool如何增加线程

在 PortableThreadPool 中有一个子类叫 GateThread,它就是专门用来增减线程的类

其底层使用一个while (true) 每隔500ms来轮询线程数量是否足够,以及一个AutoResetEvent来接收注入线程Event.

如果不够就新增

《CLR vir C#》 一书中,提过一句 CLR线程池每秒最多新增1~2个线程。结论的源头就是在这里

注意:是线程池注入线程每秒1~2个,不是每秒只能创建1~2个线程。OS创建线程的速度块多了。

眼见为实

https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Threading/PortableThreadPool.cs



眼见为实

        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的底层调用模型图



Task的底层实现主要取决于TaskSchedule,一般来说,除了UI线程外,默认是调度到线程池

眼见为实

Task.Run(() => { { Console.WriteLine("Test"); } });

其底层会自动调用Start(),Start()底层调用的TaskShedule.QueueTask().而作为实现类ThreadPoolTaskScheduler.QueueTask底层调用如下。

可以看到,默认情况下(除非你自己实现一个TaskShedule抽象类).Task的底层使用ThreadPool来管理。

有意思的是,对于长任务(Long Task),直接是用一个单独的后台线程来管理,完全不参与调度。

Task对线程池的优化

既然Task的底层是使用ThreadPool,而线程池注入速度是比较慢的。Task作为线程池的高度封装,有没有优化呢?

答案是Yes

当使用Task.Result时,底层会调用InternalWaitCore(),如果Task还未完成,会调用ThreadPool.NotifyThreadBlocked()来通知ThreadPool当前线程已经被阻塞,必须马上注入一个新线程来代替被阻塞的线程。

相对每500ms来轮询注入线程,该方式采用事件驱动,注入线程池的速度会更快。

眼见为实

点击查看代码
        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来触发注入线程的Event消息

.NET Core 线程池(ThreadPool)底层原理浅谈的更多相关文章

  1. Java8线程池ThreadPoolExecutor底层原理及其源码解析

    小侃一下 日常开发中, 或许不会直接new线程或线程池, 但这些线程相关的基础或思想是非常重要的, 参考林迪效应; 就算没有直接用到, 可能间接也用到了类似的思想或原理, 例如tomcat, jett ...

  2. Java面试必问之线程池的创建使用、线程池的核心参数、线程池的底层工作原理

    一.前言 大家在面试过程中,必不可少的问题是线程池,小编也是在面试中被问啥傻了,JUC就了解的不多.加上做系统时,很少遇到,自己也是一知半解,最近看了尚硅谷阳哥的课,恍然大悟,特写此文章记录一下!如果 ...

  3. jdk线程池ThreadPoolExecutor工作原理解析(自己动手实现线程池)(一)

    jdk线程池ThreadPoolExecutor工作原理解析(自己动手实现线程池)(一) 线程池介绍 在日常开发中经常会遇到需要使用其它线程将大量任务异步处理的场景(异步化以及提升系统的吞吐量),而在 ...

  4. 深入源码分析Java线程池的实现原理

    程序的运行,其本质上,是对系统资源(CPU.内存.磁盘.网络等等)的使用.如何高效的使用这些资源是我们编程优化演进的一个方向.今天说的线程池就是一种对CPU利用的优化手段. 通过学习线程池原理,明白所 ...

  5. Java线程池的底层实现与使用

    前言 在我们进行开发的时候,为了充分利用系统资源,我们通常会进行多线程开发,实现起来非常简单,需要使用线程的时候就去创建一个线程(继承Thread类.实现Runnable接口.使用Callable和F ...

  6. Python之路(第四十六篇)多种方法实现python线程池(threadpool模块\multiprocessing.dummy模块\concurrent.futures模块)

    一.线程池 很久(python2.6)之前python没有官方的线程池模块,只有第三方的threadpool模块, 之后再python2.6加入了multiprocessing.dummy 作为可以使 ...

  7. jdk调度任务线程池ScheduledThreadPoolExecutor工作原理解析

    jdk调度任务线程池ScheduledThreadPoolExecutor工作原理解析 在日常开发中存在着调度延时任务.定时任务的需求,而jdk中提供了两种基于内存的任务调度工具,即相对早期的java ...

  8. 线程池ThreadPool的初探

    一.线程池的适用范围 在日常使用多线程开发的时候,一般都构造一个Thread示例,然后调用Start使之执行.如果一个线程它大部分时间花费在等待某个事件响应的发生然后才予以响应:或者如果在一定期间内重 ...

  9. 高效线程池(threadpool)的实现

    高效线程池(threadpool)的实现 Nodejs编程是全异步的,这就意味着我们不必每次都阻塞等待该次操作的结果,而事件完成(就绪)时会主动回调通知我们.在网络编程中,一般都是基于Reactor线 ...

  10. 基于C++11实现线程池的工作原理

    目录 基于C++11实现线程池的工作原理. 简介 线程池的组成 1.线程池管理器 2.工作线程 3.任务接口, 4.任务队列 线程池工作的四种情况. 1.主程序当前没有任务要执行,线程池中的任务队列为 ...

随机推荐

  1. C语言三子棋

    话说自从大一学C语言后用C语言的巅峰也就是第十二届蓝桥杯了,后续开发什么的都是用的java,搞开发java这样的面向对象语言确实用着更顺手方便点.不过C语言YYDS,"C生万物"嘛 ...

  2. docker network macvlan

    ref: Docker 网络模型之 macvlan 详解,图解,实验完整 网卡也能虚拟化?网卡虚拟化技术 macvlan 详解 docker和macvlan与host互通  

  3. 五子棋AI:实现逻辑与相关背景探讨(上)

    绪论 本合集将详细讲述如何实现基于群只能遗传算法的五子棋AI,采用C++作为底层编程语言 本篇将简要讨论实现思路,并在后续的文中逐一展开 了解五子棋 五子棋规则 五子棋是一种经典的棋类游戏,规则简单却 ...

  4. Go runtime 调度器精讲(六):非 main goroutine 运行

    原创文章,欢迎转载,转载请注明出处,谢谢. 0. 前言 在 Go runtime 调度器精讲(三):main goroutine 创建 介绍了 main goroutine 的创建,文中我们说 mai ...

  5. 【QT界面美化】QT界面美化效果截图QSS+QML

    贴几个QT做的界面美化效果截图. 先来一张动图,有一些画面是QT Widgets + QSS实现的:另外一些画面是QT QML实现的. QT界面美化效果图QT QSS QML 补天云QT技术培训专家 ...

  6. CentOS 7 下通过 Cython 编写 python 扩展

    1. 安装 python 和 python-devel(没有后者,install 的时候会报错 "Scanners.c:21:20: fatal error: Python.h: No su ...

  7. USB TCPM

    USB TCPM(Type-C Port Manager)的主要作用是管理 USB Type-C 端口的连接和电源传输协议(USB Power Delivery, PD),确保设备正确识别.协商和切换 ...

  8. YashanDB发布会圆满收官,V23.1三大新品引领国产数据库技术与应用突破!

    11月8日,YashanDB 2023年度产品发布会在线上成功召开.本次产品发布会以"惟实·励新"为主题,宣布崖山数据库系统YashanDB 内核能力.产品形态.生态创新全面升级, ...

  9. vue3 + vite 分析报告 report == rollup-plugin-visualizer

    安装插件 npm i rollup-plugin-visualizer -D 配置vite.config.js 文件 [加入插件] import { defineConfig } from 'vite ...

  10. MYSQL存储过程-练习4 loop循环

    MYSQL存储过程-练习4 loop循环 创建存储过程 1 DELIMITER $$ 2 3 CREATE PROCEDURE `sp_loop`() 4 BEGIN 5 DECLARE i INT; ...