简介

上文提到,创建线程在操作系统层面有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. Linux 更新 TeX Live

    更新 TeX Live 假设你的旧版 TeX Live 版本号为 2023,新版 TeX Live 版本号为 2024.你需要在下面的命令中相应地更改实际版本号.TeX Live 版本可以通过 tlm ...

  2. TS中的声明文件

    TS中的声明文件 .d.ts 的作用是为了在TS中使用js文件,但是js文件没有类型,ts又是一个类型严格的语言.所以为了在ts中使用js第三方包,或者自定义Js模块.便由此引出了.d.ts文件. 需 ...

  3. fluent python-chap3-1

    class collections.OrderedDict([items]) 返回一个 dict 子类的实例,它具有专门用于重新排列字典顺序的方法. """ move_t ...

  4. vue中如何使用JSX?

    JSX是什么? JSX 是一种 Javascript 的语法扩展,JSX = Javascript + XML,即在 Javascript 里面写 XML,因为 JSX 的这个特性,所以他即具备了 J ...

  5. ScanFormer:逐层抵达目标,基于特征金字塔的指代表达理解框架 | CVPR'24

    指代表达理解(REC)旨在在图像中定位由自由形式自然语言描述指定的目标对象.尽管最先进的方法取得了令人印象深刻的性能,但它们对图像进行了密集感知,包含与语言查询无关的多余视觉区域,导致额外的计算开销. ...

  6. 第5天:基础入门-反弹SHELL&不回显带外&正反向连接&防火墙出入站&文件下载

    文件上传下载-解决无图形化&解决数据传输 命令生成:https://forum.ywhack.com/bountytips.php?download 反弹shell 以参照物为准,以Linux ...

  7. LNMP 和 LAMP 对比 (仅供参考)

    Nginx 性能稳定.功能丰富.运维简单.处理静态文件速度快且消耗系统资源极少. Apache 是 LAMP 架构最核心的 Web Server,开源.稳定.模块丰富是 Apache 的优势.但 Ap ...

  8. MySQL 切换 Oracle 问题整理

    MySQL 通常小写,Oracle 默认大写 ,查询过程中需加双引号,或者直接将MySQL 字段转换成大写 Springboot 配置 oracle连接 spring: datasource: url ...

  9. c++可变模板参数

    在C++中的可变模板参数使用省略号 ... 来表示一个参数包(Parameter Pack),其具体位置决定了这个包是模板参数包还是函数参数包,以及如何进行参数展开. 1. 模板参数包:c... Ar ...

  10. JIT编译选项

    JIT(Just-In-Time)优化在编译过程中有多种编译选项可以支持,不同语言和平台可能有不同的实现.以通用的 JIT 编译器为例,以下是一些常见的编译选项: 编译级别(Compilation L ...