聊一聊 C#线程池 的线程动态注入 (上)

news/2024/12/23 13:33:06/文章来源:https://www.cnblogs.com/huangxincheng/p/18623762

一:背景

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下的注入优化。
图片名称

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/857429.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

IT项目经理力推的2024年10款项目管理工具

在数字化时代,IT项目经理面临着日益复杂的挑战,尤其是在项目管理工具的选择上。为了帮助项目经理在2024年更高效地管理项目,本文将介绍10款备受推崇的管理工具,这些工具不仅功能强大,而且能够适应各种项目管理需求。无论你是初学者还是经验丰富的项目经理,这些工具都能为…

最近公共祖先(LCA)笔记

最近公共祖先(LCA)笔记 【模板】最近公共祖先(LCA) 题目入口 题目描述 如题,给定一棵有根多叉树,请求出指定两个点直接最近的公共祖先。 输入格式 第一行包含三个正整数 \(N,M,S\),分别表示树的结点个数、询问的个数和树根结点的序号。 接下来 \(N-1\) 行每行包含两个正…

Windows 网络安全系统框架环境搭建

网络安全虚拟机框架环境搭建操作系统Vmware Workstation (17.5.2 build-23775571) Windows 11 300G硬盘空间(C、D、E) 8G内存 分三个盘,其中D盘用于安装软件、E盘用于工作Win11 跳过联网激活 shift+F10 或Fn+shift+F10 打开cmd窗口,执行oobe\bypassNRO,系统自动重启后即…

【日记】昨天才找到的头绳,今天就不小心给洗了(947 字)

正文前些周有个周末研究了一天的护发素(虽然现在已经忘光了),今天研究了半天到一天的沐浴露(所以打算写点什么记下来,免得又忘了)。研究完了之后,发现母亲原来给我买的那瓶沐浴露,是把能踩得雷都踩了一遍……甲基氯异噻唑啉酮(CMIT)和甲基异噻唑啉酮(MIT)都有,不过…

【Java编程】定时任务实现原理详解

一、摘要 在很多业务的系统中,我们常常需要定时的执行一些任务,例如定时发短信、定时变更数据、定时发起促销活动等等。 在上篇文章中,我们简单的介绍了定时任务的使用方式,不同的架构对应的解决方案也有所不同,总结起来主要分单机和分布式两大类,本文会重点分析下单机的…

manim边学边做--移动动画

在Manim中,其实直线移动的动画非常简单,每个Mobject对象都有animate属性, 通过obj.animate.shift()或者obj.animate.move_to()很容易将对象从一个位置移往另一个位置。 不过,如果要更复杂的移动路线,那么animate属性的移动方法就无法满足了。 本篇介绍Manim中的两个处理复…

科来网络分析系统:国产网络数据包分析工具推荐

wireshark作为数据分析过程中必备的工具,深受好评。但是其使用起来有一定的门槛,本文介绍的:科来网络分析系统(技术交流版)。在某些方面相比于wireshark更有优势。附数据包分析案例 1、下载安装: 登录官网 https://www.colasoft.com.cn/downloads/capsa2、启动3、选择工作…

PDF转换工具Marker、MinerU、Markitdown对比分析

1 文件格式插件MarkerMinerUMarkitdownmagic-pdfmagic-doc支持的文件格式PDF、EPUB 和 MOBIPDFPPT、PPTX、DOC、DOCX、PDFPDF, PowerPoint, Word, Excel Images (EXIF metadata and OCR), Audio (EXIF metadata and speech transcription), HTML, CSV, JSON, XML, ZIP f…

【附源码】JAVA在线拍卖系统+SpringBoot+VUE+前后端分离

学弟,学妹好,我是爱学习的学姐,今天带来一款优秀的项目:在线拍卖系统 。 本文介绍了系统功能与部署安装步骤,如果您有任何问题,也请联系学姐,偶现在是经验丰富的程序员! 一. 系统演示 管理后台-截图拍卖前端-截图视频演示 https://githubs.xyz/show/307.mp4 二. 系统概…

PostgreSQL 的历史

title: PostgreSQL 的历史 date: 2024/12/23 updated: 2024/12/23 author: cmdragon excerpt: PostgreSQL 是一款功能强大且广泛使用的开源关系型数据库管理系统。其历史可以追溯到1986年,当时由加州大学伯克利分校的一个研究团队开发。文章将深入探讨 PostgreSQL 的起源、发…

【社工钓鱼】手法总结

1479394864616213 七芒星实验室 2024年12月23日 07:03 四川 一、rlo文件名翻转 简介:全名Right-to-Left Override,本质是一串Unicode字符,编码0x202E,本身不可见,插入之后会让在他之后的字符串从右往左重新排列,本意是用来支持一些从右往左写的语言的文字,比如阿拉伯语、…