聊一聊 C#线程池 的线程动态注入 (中)
一:背景
1. 讲故事
上一篇我们用 Thread.Sleep 的方式演示了线程池饥饿场景下的动态线程注入,可以观察到大概 1s 产生 1~2 个新线程,很显然这样的增长速度扛不住上游请求对线程池的DDOS攻击,导致线程池队列越来越大,但C#团队这么优秀,能优化的地方绝对会给大家尽可能的优化,比如这篇我们聊到的 Task.Result 场景下的注入。
二:Task.Result 角度下的动态注入
1. 测试代码
为了直观的体会到优化效果,先上一段测试代码观察一下。
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}: 这是耗时任务");
try
{
var client = new HttpClient();
var content = client.GetStringAsync("https://youtube.com").Result;
Console.WriteLine(content.Length);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}, i);
}
Console.ReadLine();
}

从卦象上来看大概1s产生4个新线程,再仔细看的话大概是250ms一个,虽然250不大好听,但不管怎么说确实比 Thread.Sleep 场景下只产生 1~2 个线程要快了好几倍,以终为始,我们再反向的看下这个优化的底层逻辑在哪?
2. 底层逻辑在哪里
还是那句话,千言万语不抵一张图,流程图大概如下:

接下来解释下其中的几个元素。
- NotifyThreadBlocked
这是主动通知 GateThread 线程赶紧醒来,通过上一篇的知识大家应该知道 GateThread 会500ms一次被动唤醒,但为了提速不可能再这么干了,需要让人强制唤醒它,修剪后的源码如下:
private bool SpinThenBlockingWait(int millisecondsTimeout, CancellationToken cancellationToken)
{
var mres = new SetOnInvokeMres();
AddCompletionAction(mres, addBeforeOthers: true);
bool notifyWhenUnblocked = ThreadPool.NotifyThreadBlocked();
var returnValue = mres.Wait((int)(millisecondsTimeout - elapsedTimeTicks), cancellationToken);
return returnValue;
}
public bool NotifyThreadBlocked()
{
GateThread.Wake(this);
return true;
}
public static void Wake(PortableThreadPool threadPoolInstance)
{
DelayEvent.Set();
}
卦中的 DelayEvent.Set(); 正是强制唤醒 GateThread 的 event 事件。
- HasBlockingAdjustmentDelayElapsed
GateThread 是注入线程的官方通道,那到底要不要注入线程呢?肯定少不了一些判断,其中一个判断就是当前的延迟周期是否超过了 250ms,这个250ms的阈值最终由 BlockingConfig.MaxDelayMs 变量指定,这是能否调用 CreateWorkerThread方法需要闯的一个关口,参考代码如下:
private static class BlockingConfig
{
MaxDelayMs =(uint) AppContextConfigHelper.GetInt32Config(
"System.Threading.ThreadPool.Blocking.MaxDelayMs",
250,
false);
}
private static void GateThreadStart()
{
while (true)
{
bool wasSignaledToWake = DelayEvent.WaitOne((int)delayHelper.GetNextDelay(currentTimeMs));
currentTimeMs = Environment.TickCount;
do
{
previousDelayElapsed = delayHelper.HasBlockingAdjustmentDelayElapsed(currentTimeMs, wasSignaledToWake);
if (pendingBlockingAdjustment == PendingBlockingAdjustment.WithDelayIfNecessary && !previousDelayElapsed)
{
break;
}
uint nextDelayMs = threadPoolInstance.PerformBlockingAdjustment(previousDelayElapsed);
} while (false);
}
}
public bool HasBlockingAdjustmentDelayElapsed(int currentTimeMs, bool wasSignaledToWake)
{
if (!wasSignaledToWake && _adjustForBlockingAfterNextDelay)
{
return true;
}
uint elapsedMsSincePreviousBlockingAdjustmentDelay = (uint)(currentTimeMs - _previousBlockingAdjustmentDelayStartTimeMs);
return elapsedMsSincePreviousBlockingAdjustmentDelay >= _previousBlockingAdjustmentDelayMs;
}
从上面的代码可以看到一旦 previousDelayElapsed =false 就直接 break 了,不再调用PerformBlockingAdjustment 方法来闯第二个关口。
- PerformBlockingAdjustment
一旦满足了250ms阈值之后,接下来就需要观察ThreadPool当前的负载能力,由内部的 ThreadCounts 提供支持,比如 NumProcessingWork 表示当前线程池正在处理的任务数, NumThreadsGoal 表示线程不要超过此上限值,如果超过了就进入动态注入阶段,参考代码如下:
private struct ThreadCounts
{
public short NumProcessingWork;
public short NumExistingThreads;
public short NumThreadsGoal;
}
有了这个基础之后,接下来再上一段注入线程需要满足的第二个关口。
private static void GateThreadStart()
{
uint nextDelayMs = threadPoolInstance.PerformBlockingAdjustment(previousDelayElapsed);
}
private uint PerformBlockingAdjustment(bool previousDelayElapsed)
{
var nextDelayMs = PerformBlockingAdjustment(previousDelayElapsed, out addWorker);
if (addWorker)
{
WorkerThread.MaybeAddWorkingWorker(this);
}
return nextDelayMs;
}
private uint PerformBlockingAdjustment(bool previousDelayElapsed, out bool addWorker)
{
if (counts.NumProcessingWork >= numThreadsGoal && _separated.numRequestedWorkers > 0)
{
addWorker = true;
}
}
从卦中代码可以看到,一旦线程池中 处理的任务数 >= 线程上限值,这就表示当前线程池正在满负荷的跑,numRequestedWorkers>0 表示有新任务来了需要线程来处理,所以这两组条件一旦满足,就必须要创建新线程。
3. 如何眼见为实
刚才啰嗦了那么多,那如何眼见为实呢?非常简单,还是用 dnspy 的断点日志功能观察,我们下三个断点。
- 第一个条件 HasBlockingAdjustmentDelayElapsed 处增加
1. {!wasSignaledToWake} {this._adjustForBlockingAfterNextDelay}, 延迟时间:{currentTimeMs - this._previousBlockingAdjustmentDelayStartTimeMs} ,上一次延迟:{_previousBlockingAdjustmentDelayMs}。

- 第二个条件 PerformBlockingAdjustment 处增加
2. 正在处理任务数:{threadCounts.NumProcessingWork} ,合适线程数:{num},是否要新增线程:{this._separated.numRequestedWorkers>0}。

- 线程创建 WorkerThread.CreateWorkerThread 处增加
3. 已成功创建线程。

最后把程序跑起来,观察 output窗口 的结果,非常清爽,吉卦。

三:总结
采用主动通知的方式唤醒GateThread可以让每秒线程注入数由原来的 1~2 个提升到 4 个,虽然有所优化,但面对上游洪水猛兽般的请求,很显然也是杯水车薪,最终还是酿成了线程饥饿的悲剧,下一篇我们继续研究如何让线程注入的快一点,再快一点。。。
聊一聊 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 ...
随机推荐
- maven的pom.xml基础配置
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://mave ...
- 使用 vue2 + element-ui 登录的时候的逻辑
1. 自动校验表单逻辑 // 1. 自动表单验证 try { // 这个形式自动表单验证麻烦 // this.$refs.loginForm.validate((valid)=>{ ... }) ...
- 使用AVX2指令集加速推荐系统MMR层余弦相似度计算
原文:blog.fanscore.cn/a/62/ 1. 背景 前一段时间公司上线了一套Go实现的推荐系统,上线后发现MMR层虽然只有纯计算但耗时十分离谱,通过pprof定位问题所在之后进行了优化,虽 ...
- kotlin集合——>迭代器、区间与数列
1.迭代器 对于遍历集合元素,Kotlin 标准库支持 迭代器 的常用机制⸺对象可按顺序提供对元素的访问权限,而 不会暴露集合的底层结构.当需要逐个处理集合的所有元素(例如打印值或对其进行类似更新)时 ...
- 2024年开源API工具盘点,覆盖API全生命周期
2024年经济持续低迷,本文整理一些免费的开源工具,旨在帮助企业组织降低工具的支出成本,能用免费的何必用付费的呢(狗头)? 如何高效地管理API的全生命周期--从设计.开发.测试.部署到监控和优化,已 ...
- .NET无侵入式对象池解决方案
Pooling(https://github.com/inversionhourglass/Pooling),编译时对象池组件,在编译时将指定类型的new操作替换为对象池操作,简化编码过程,无需开发人 ...
- 征服 Docker 镜像访问限制:KubeSphere v3.4.1 成功部署全攻略
近期,KubeSphere 社区的讨论中频繁出现关于 Docker 官方镜像仓库访问受限的问题. 本文旨在为您提供一个详细的指南, 展示在 Docker 官方镜像访问受限的情况下,如何通过 KubeK ...
- 向AWS迁移系统
向AWS迁移系统的考虑事项: 1.理解现在应用的架构和运行环境: 使用的OS,软件版本,依赖库,底层硬件,数据库,资源使用用量.性能等非功能要件. 基于这些信息设计在AWS上的架构,使用Ins ...
- 全面解释人工智能LLM模型的真实工作原理(完结)
前一篇:<全面解释人工智能LLM模型的真实工作原理(三)> 序言: 本节作为整篇的收官之作,自然少不了与当今最先进的AI模型相呼应.这里我们将简单介绍全球首家推动人工智能生成人类语言的公司 ...
- swiper + ts 类型报错
swiper + ts 类型报错 "swiper": "^9.4.1" 版本号 原因 修改 tsconfig.json 文件下面的 moduleResoluti ...