聊一聊 C#线程池 的线程动态注入 (上)
一:背景
1. 讲故事
在线程饥饿的场景中,我们首先要了解的就是线程是如何动态注入的?其实现如今的ThreadPool内部的实现逻辑非常复杂,而且随着版本的迭代内部逻辑也在不断的变化,有时候也没必要详细的去了解,只需在稍微宏观的角度去理解一下即可,我准备用三篇来详细的聊一聊线程注入的流程走向来作为线程饥饿的铺垫系列,这篇我们先从 Thread.Sleep 的角度观察线程的动态注入。
二:Sleep 角度下的动态注入
1. 测试代码
为了方便研究,我们用 Thread.Sleep 的方式阻塞线程池线程,然后观察线程的注入速度,参考代码如下:
static void Main(string[] args)
{
for (int i = 0; i < 10000; i++)
{
ThreadPool.QueueUserWorkItem((idx) =>
{
Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff")} -> {idx}: 这是耗时任务");
Thread.Sleep(int.MaxValue);
}, i);
}
Console.ReadLine();
}

仔细观察卦中的输出,除了初始的12个线程喷涌而出,后面你会发现它的线程动态注入有时候大概是 500ms 一次,有时候会是 1000ms 一次,所以我们可以得到一个简单的结论:Thread.Sleep 场景下1s 大概会动态注入1~2个线程。
有了这个结论之后,接下来我们探究下它的底层逻辑在哪?
2. 底层代码逻辑在哪
千言万语不及一张图,截图如下:

接下来我们来聊一下卦中的各个元素吧。
- GateThread
在 PortableThreadPool 中有一个 GateThread 类,专门掌管着线程的动态注入,默认情况下它大概是 500ms 被唤醒一次。这个是有很多逻辑源码支撑的。
private static class GateThread
{
public const uint GateActivitiesPeriodMs = 500;
private static void GateThreadStart()
{
while (true)
{
bool wasSignaledToWake = DelayEvent.WaitOne((int)delayHelper.GetNextDelay(currentTimeMs));
...
}
}
public uint GetNextDelay(int currentTimeMs)
{
uint elapsedMsSincePreviousGateActivities = (uint)(currentTimeMs - _previousGateActivitiesTimeMs);
uint nextDelayForGateActivities =
elapsedMsSincePreviousGateActivities < GateActivitiesPeriodMs
? GateActivitiesPeriodMs - elapsedMsSincePreviousGateActivities
: 1;
...
}
}
- SufficientDelaySinceLastDequeue
这个方法是用来判断任务最后一次出队的时间,即内部的lastDequeueTime 字段,这也是为什么有时候是1个周期(500ms),有时候是2个周期的底层原因,如果在一个周期内判断lastDequeueTime(490ms)<=500ms,那么在下一个周期内判断最后一次出队的时间自然就是 490ms+500ms,所以这就是为什么 Console 上显示大约 1s 的间隔的原因了,下面的代码演示了 lastDequeueTime 是如何存取的。
private static void GateThreadStart()
{
if (!disableStarvationDetection &&
threadPoolInstance._pendingBlockingAdjustment == PendingBlockingAdjustment.None &&
threadPoolInstance._separated.numRequestedWorkers > 0 &&
SufficientDelaySinceLastDequeue(threadPoolInstance))
{
bool addWorker = false;
if (addWorker)
{
WorkerThread.MaybeAddWorkingWorker(threadPoolInstance);
}
}
}
private static bool SufficientDelaySinceLastDequeue(PortableThreadPool threadPoolInstance)
{
uint delay = (uint)(Environment.TickCount - threadPoolInstance._separated.lastDequeueTime);
uint minimumDelay;
if (threadPoolInstance._cpuUtilization < CpuUtilizationLow)
{
minimumDelay = GateActivitiesPeriodMs;
}
else
{
minimumDelay = (uint)threadPoolInstance._separated.counts.NumThreadsGoal * DequeueDelayThresholdMs;
}
return delay > minimumDelay;
}
private static void WorkerDoWork(PortableThreadPool threadPoolInstance, ref bool spinWait)
{
bool alreadyRemovedWorkingWorker = false;
while (TakeActiveRequest(threadPoolInstance))
{
threadPoolInstance._separated.lastDequeueTime = Environment.TickCount;
if (!ThreadPoolWorkQueue.Dispatch())
{
}
}
}
- CreateWorkerThread
这个方法是用来创建线程的主体逻辑,在线程池中由上层的 MaybeAddWorkingWorker 调用,参考如下:
internal static void MaybeAddWorkingWorker(PortableThreadPool threadPoolInstance)
{
while (toCreate > 0)
{
CreateWorkerThread();
toCreate--;
}
}
private static void CreateWorkerThread()
{
Thread workerThread = new Thread(s_workerThreadStart);
workerThread.IsThreadPoolThread = true;
workerThread.IsBackground = true;
workerThread.UnsafeStart();
}
这里有一个注意点:上面的 while (toCreate > 0) 代码预示着一个周期内(500ms)可能会连续创建多个工作线程,但在饥饿的大多数情况下都是toCreate=1的情况。
3.如何眼见为实
说了这么多,能不能用一些手段让我眼见为实呢?要想眼见为实也不难,可以用 dnspy 断点日志功能观察即可,分别在如下三个方法上下断点。
- delayHelper.GetNextDelay
在此处下断点的目的用于观察 GateThread 的唤醒周期时间,截图如下:

- SufficientDelaySinceLastDequeue
这里下断点主要是观察当前的延迟如果超过 500ms 时是否真的会通过 CreateWorkerThread 创建线程。截图如下:

- WorkerThread.CreateWorkerThread
最后我们在 MaybeAddWorkingWorker 方法的底层的线程创建方法 CreateWorkerThread 中下一个断点。

所有的埋点下好之后,我们让程序跑起来,观察 output 窗口的输出。

从输出窗口中可以清晰的看到以500ms为界限判断啥时该创建,啥时不该创建。
三:总结
可能有些朋友很感慨,线程的动态注入咋怎么慢?1s才1-2个,难怪会出现线程饥饿。。。哈哈,下一篇我们聊一聊Task.Result下的注入优化。

聊一聊 C#线程池 的线程动态注入 (上)的更多相关文章
- Java多线程系列 JUC线程池03 线程池原理解析(二)
转载 http://www.cnblogs.com/skywang12345/p/3509954.html http://www.cnblogs.com/skywang12345/p/351294 ...
- Java多线程系列 JUC线程池02 线程池原理解析(一)
转载 http://www.cnblogs.com/skywang12345/p/3509960.html ; http://www.cnblogs.com/skywang12345/p/35099 ...
- java并发编程(十七)----(线程池)java线程池架构和原理
前面我们简单介绍了线程池的使用,但是对于其如何运行我们还不清楚,Executors为我们提供了简单的线程工厂类,但是我们知道ThreadPoolExecutor是线程池的具体实现类.我们先从他开始分析 ...
- 由浅入深理解Java线程池及线程池的如何使用
前言 多线程的异步执行方式,虽然能够最大限度发挥多核计算机的计算能力,但是如果不加控制,反而会对系统造成负担.线程本身也要占用内存空间,大量的线程会占用内存资源并且可能会导致Out of Memory ...
- 基于线程池的线程管理(BlockingQueue生产者消费者方式)实例
1.线程池管理类: public class ThreadPoolManager { private static ThreadPoolManager instance = new ThreadPoo ...
- -1-5 java 多线程 概念 进程 线程区别联系 java创建线程方式 线程组 线程池概念 线程安全 同步 同步代码块 Lock锁 sleep()和wait()方法的区别 为什么wait(),notify(),notifyAll()等方法都定义在Object类中
本文关键词: java 多线程 概念 进程 线程区别联系 java创建线程方式 线程组 线程池概念 线程安全 同步 同步代码块 Lock锁 sleep()和wait()方法的区别 为什么wait( ...
- Java多线程、线程池和线程安全整理
多线程 1.1 多线程介绍 进程指正在运行的程序.确切的来说,当一个程序进入内存运行,即变成一个进程,进程是处于运行过程中的程序,并且具有一定独立功能. 1.2 Thread类 通 ...
- ReentrantLock+线程池+同步+线程锁
1.并发编程三要素? 1)原子性 原子性指的是一个或者多个操作,要么全部执行并且在执行的过程中不被其他操作打断,要么就全部都不执行. 2)可见性 可见性指多个线程操作一个共享变量时,其中一个线程对变量 ...
- .NET线程池最大线程数的限制-记一次IIS并发瓶颈
.NET ThreadPool 最大线程数的限制 IIS并发瓶颈,有几个地方,IIS线程池的最大队列数,工作进程数,最大并发数.这些这里就不展开.主要是最近因为过度使用Task 导致的线程数占用过多, ...
- 根据CPU核心数确定线程池并发线程数
一.抛出问题 关于如何计算并发线程数,一般分两派,来自两本书,且都是好书,到底哪个是对的?问题追踪后,整理如下: 第一派:<Java Concurrency in Practice>即&l ...
随机推荐
- NICE与静态优先级的关系
在Linux系统中,nice值和静态优先级用于控制进程调度的优先级,但它们的范围和含义有所不同.让我们详细解释一下两者的区别和联系. 1. Nice值 范围:nice值的范围是从 -20 到 19. ...
- nestjs 登录和验证码结合验证 svgCaptcha 包 session 会话标识
// ps: 现在用户验证使用 token jwt 了 代替了 session // session 是服务器为每个用户建立的唯一标识 以区分用户 会话标识 // session 是express中的 ...
- HRM平台的登录页的背景图片- scss
.login-container { // 设置背景图片 background-image: url("~@/assets/common/login.jpg"); ba ...
- apisix~为自定义插件设计一个configmap脚本
configMap Kubernetes 中的 ConfigMap 是一种用来存储配置数据的 API 资源,它允许您将配置信息以键值对的形式保存,并在容器中使用这些配置信息.ConfigMap 提供了 ...
- 在 KubeSphere 中开启新一代云原生数仓 Databend
作者:尚卓燃(https://github.com/PsiACE),Databend 研发工程师,Apache OpenDAL (Incubating) PPMC. 前言 Databend 是一款完全 ...
- 自如月租计算 ziroom
前言 自如的房子月租看似不高,实际上它是收中介费的,加上中介费和未满一年的押金,房租其实非常高. 普通中介费一般收1个月,自如的中介费美名其曰服务费(除了网费想不到有什么用的),一年为1.2个月租金. ...
- HTML5+CSS3+JavaScript网页实战
1. HTML5基础 HTML5,作为构建和呈现网页内容的标准标记语言,带来了许多革命性的变化.它不仅提供了更加语义化的标签,使得网页内容更具可读性和可访问性,还增加了对多媒体的原生支持,无需依赖第三 ...
- Java面试题中高级进阶(JVM篇Java垃圾回收)
前言 本来想着给自己放松一下,刷刷博客,突然被几道面试题难倒!说说Java对象创建过程?知道类的生命周期吗?简述Java的对象结构?如何判断对象可以被回收?JVM的永久代中会发生垃圾回收么?你知道哪些 ...
- 利用DbgHelp获取线程的栈回溯信息
当线程发生异常时,我们如果可以记录下来异常线程的堆栈信息,那么对于我们后续问题处理将会有极大的帮助.这里记录一个操作方法. 1 #include <iostream> 2 #include ...
- 超实用的SpringAOP实战之日志记录
本文主要以日志记录作为切入点,来讲解Spring AOP在实际项目开发中怎样更好的使项目业务代码更加简洁.开发更加高效. 日志处理只是AOP其中一种应用场景,当你掌握了这一种场景,对于其它应用场景也可 ...