一:背景

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. 底层代码逻辑在哪

千言万语不及一张图,截图如下:

接下来我们来聊一下卦中的各个元素吧。

  1. 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;
...
}
}
  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())
{
}
}
}
  1. 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 断点日志功能观察即可,分别在如下三个方法上下断点。

  1. delayHelper.GetNextDelay

在此处下断点的目的用于观察 GateThread 的唤醒周期时间,截图如下:

  1. SufficientDelaySinceLastDequeue

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

  1. WorkerThread.CreateWorkerThread

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

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

从输出窗口中可以清晰的看到以500ms为界限判断啥时该创建,啥时不该创建。

三:总结

可能有些朋友很感慨,线程的动态注入咋怎么慢?1s才1-2个,难怪会出现线程饥饿。。。哈哈,下一篇我们聊一聊Task.Result下的注入优化。

聊一聊 C#线程池 的线程动态注入 (上)的更多相关文章

  1. Java多线程系列 JUC线程池03 线程池原理解析(二)

    转载  http://www.cnblogs.com/skywang12345/p/3509954.html  http://www.cnblogs.com/skywang12345/p/351294 ...

  2. Java多线程系列 JUC线程池02 线程池原理解析(一)

    转载  http://www.cnblogs.com/skywang12345/p/3509960.html ; http://www.cnblogs.com/skywang12345/p/35099 ...

  3. java并发编程(十七)----(线程池)java线程池架构和原理

    前面我们简单介绍了线程池的使用,但是对于其如何运行我们还不清楚,Executors为我们提供了简单的线程工厂类,但是我们知道ThreadPoolExecutor是线程池的具体实现类.我们先从他开始分析 ...

  4. 由浅入深理解Java线程池及线程池的如何使用

    前言 多线程的异步执行方式,虽然能够最大限度发挥多核计算机的计算能力,但是如果不加控制,反而会对系统造成负担.线程本身也要占用内存空间,大量的线程会占用内存资源并且可能会导致Out of Memory ...

  5. 基于线程池的线程管理(BlockingQueue生产者消费者方式)实例

    1.线程池管理类: public class ThreadPoolManager { private static ThreadPoolManager instance = new ThreadPoo ...

  6. -1-5 java 多线程 概念 进程 线程区别联系 java创建线程方式 线程组 线程池概念 线程安全 同步 同步代码块 Lock锁 sleep()和wait()方法的区别 为什么wait(),notify(),notifyAll()等方法都定义在Object类中

     本文关键词: java 多线程 概念 进程 线程区别联系 java创建线程方式 线程组 线程池概念 线程安全 同步 同步代码块 Lock锁  sleep()和wait()方法的区别 为什么wait( ...

  7. Java多线程、线程池和线程安全整理

    多线程 1.1      多线程介绍 进程指正在运行的程序.确切的来说,当一个程序进入内存运行,即变成一个进程,进程是处于运行过程中的程序,并且具有一定独立功能. 1.2      Thread类 通 ...

  8. ReentrantLock+线程池+同步+线程锁

    1.并发编程三要素? 1)原子性 原子性指的是一个或者多个操作,要么全部执行并且在执行的过程中不被其他操作打断,要么就全部都不执行. 2)可见性 可见性指多个线程操作一个共享变量时,其中一个线程对变量 ...

  9. .NET线程池最大线程数的限制-记一次IIS并发瓶颈

    .NET ThreadPool 最大线程数的限制 IIS并发瓶颈,有几个地方,IIS线程池的最大队列数,工作进程数,最大并发数.这些这里就不展开.主要是最近因为过度使用Task 导致的线程数占用过多, ...

  10. 根据CPU核心数确定线程池并发线程数

    一.抛出问题 关于如何计算并发线程数,一般分两派,来自两本书,且都是好书,到底哪个是对的?问题追踪后,整理如下: 第一派:<Java Concurrency in Practice>即&l ...

随机推荐

  1. 九问 GBase | 如何看待“科技制裁”?如何助力中国数据库国产化落地?

    导读: Oracle.SAP.Apple.Google.Github等国际科技巨头纷纷宣布停止在俄罗斯业务,英特尔.AMD.戴尔等科技企业也被曝已中断向俄供货.当全面科技制裁来临,俄罗斯将如何应对此次 ...

  2. document.designMode 设计模式

    document.designMode 的默认值是 off 关闭: 在控制台输入 document.designMode = 'on' 就可以编辑页面内容:

  3. DOMException: Failed to execute 'setRequestHeader' on 'XMLHttpRequest': 'function (header, parser) { header = normalizeHeader(header);

    场景:token过期,然后更新了token 重新发起请求获取数据 : 代码:使用上一次的错误请求配置报错     return request(error.config) : 解决 :  return ...

  4. 3.flask 源码解析:路由

    目录 一.flask 源码解析:路由 1.1 构建路由规则 1.2 werkzeug 路由逻辑 1.3 flask 路由实现 1.4 match 实现 Flask 源码分析完整教程目录:https:/ ...

  5. Linux查看显卡 GPU信息

    1.Linux查看显卡信息: 1.1查询显卡信息 lspci | grep -i vga 前边的序号 "04:00.0″是显卡的代号 ASPEED Technology, Inc. ASPE ...

  6. 《刚刚问世》系列初窥篇-Java+Playwright自动化测试-3-启动浏览器(详细教程)

    1.简介 通过前边两篇文章跟随宏哥学习想必到这里已经将环境搭建好了,今天就在Java项目搭建环境中简单地实践一下: 启动两大浏览器.按市场份额来说,全球前三大浏览器是:IE.Firefox.Chrom ...

  7. Machine Learning week_2 Multivariate Prameters Regression

    目录 1 Multivariate Prameters Regression 1.1 Reading Multiple Features 1.2 Gradient Descent For Multip ...

  8. 结构体(C语言)

    目录 1. 结构体类型的声明 1.1 结构体回顾 1.1.1 结构的声明 1.1.2 结构体变量的创建和初始化 1.2 结构的特殊声明 1.3 结构的自引用 2. 结构体内存对齐 2.1 对齐规则 2 ...

  9. Python自带difflib模块

    官方文档:https://docs.python.org/3/library/difflib.html difflib模块的作用是比对文本之间的差异,且支持输出可读性比较强的HTML文档,与Linux ...

  10. 锋利的在线诊断工具——Arthas

    导航 前言 火线告警,CPU飚了 服务重启,迅速救火 黑盒:无尽的猜测和不安 Arthas:锋利的Java诊断工具 在线追踪Cpu占比高的代码段 代码重构,星夜上线,稳了 结语 参考 肮脏的代码必须重 ...