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

打印 上一主题 下一主题

主题 858|帖子 858|积分 2574

一:背景

1. 讲故事

前面二篇我们聊到了 Thread.Sleep 和 Task.Result 场景下的线程注入逻辑,在线程饥饿的情况下注入速度都不是很理想,那怎么办呢?有没有更快的注入速度,这篇作为 动态注入 的终结篇,我个人总结如下两种方法,当然可能有更多的路子,知道的朋侪可以在下面留言。
二:提高注入速度的两种方法

1. 低落GateThread的延迟时间

上一篇跟大家聊过 Result 默认情况下GateThread每秒会注入4个,底层逻辑是由 Blocking.MaxDelayMs=250ms 变量控制的,言外之意就是能不能减少这个变量的值呢?当然可以的,这里我们改成 100ms,参考代码如下:
  1.         static void Main(string[] args)
  2.         {
  3.             AppContext.SetData("System.Threading.ThreadPool.Blocking.MaxDelayMs", 100);
  4.             for (int i = 0; i < 10000; i++)
  5.             {
  6.                 ThreadPool.QueueUserWorkItem((idx) =>
  7.                 {
  8.                     Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff")} -> {idx}: 这是耗时任务");
  9.                     try
  10.                     {
  11.                         var client = new HttpClient();
  12.                         var content = client.GetStringAsync("https://youtube.com").Result;
  13.                         Console.WriteLine(content.Length);
  14.                     }
  15.                     catch (Exception ex)
  16.                     {
  17.                         Console.WriteLine(ex.Message);
  18.                     }
  19.                 }, i);
  20.             }
  21.             Console.ReadLine();
  22.         }
复制代码
现在我们照旧用上一篇的方法在如下三个方法 HasBlockingAdjustmentDelayElapsed,PerformBlockingAdjustment,CreateWorkerThread  上埋日记断点,埋好之后运行程序观察。

从卦中的输出结果看,注入速度明显快了很多,判断阈值也从 250ms 变成了 100ms,每秒能注入7~8个线程,所以这是一个简单粗暴的提速方法。
2. 提高 MinThreads 的阈值

看过上两篇的朋侪应该知道,我用过 喷涌而出 四个字来形容前 12个线程,这里的12是因为我的机器是 12 核,言外之意就是为什么要设置12呢?我能不能给它提升到 120,1200甚至更高的 12000 呢?如许线程的注入速度不是更快吗?有了这个想法赶紧上一段代码,参考如下:
  1.         static void Main(string[] args)
  2.         {
  3.             ThreadPool.SetMinThreads(10000, 10);
  4.             for (int i = 0; i < 10000; i++)
  5.             {
  6.                 ThreadPool.QueueUserWorkItem((idx) =>
  7.                 {
  8.                     Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff")} -> {idx}: 这是耗时任务");
  9.                     Thread.Sleep(int.MaxValue);
  10.                 }, i);
  11.             }
  12.             Console.ReadLine();
  13.         }
复制代码

从卦中看,直接秒了这个 10000 个任务,但不要忘了你的程序此时有1w个线程,如果是32bit程序大概率因为假造地址不足直接崩了,如果是 64bit 可能也会导致非常可观的内存占用。
有些人可能对底层逻辑感兴趣,我特意花了点时间绘了一张图来形貌底层的运转逻辑。


之所以能快速的产生新线程,核心判断条件是 numProcessingWork  (int)this._maxThreads)            {                return false;            }            if (ioCompletionThreads > (int)this._legacy_maxIOCompletionThreads)            {                return false;            }        }[/code]从卦中代码可以看到 ioCompletionThreads 默认最大值为 1000,如果你设置的值大于 1000 的话,那前面的 workerThreads 即是白设置了。。。这就很无语了。。。 如果参数有误,你完全可以抛出一个异常来告诉我,,,而不是偷偷的掩埋错误信息,导致程序出现了我意想不到的行为。。。
为了凑篇幅,我再说一个有意思的参数 DebugBreakOnWorkerStarvation,它可以用来捕获 线程饥饿 的第一现场,底层逻辑是C#团队在代码里埋了一个钩子,参考如下:
  1.         public class PortableThreadPool
  2.         {
  3.             private short _minThreads;
  4.             private short _maxThreads;
  5.             private short _legacy_maxIOCompletionThreads;
  6.             private const short DefaultMaxWorkerThreadCount = MaxPossibleThreadCount;
  7.             private const short MaxPossibleThreadCount = short.MaxValue;
  8.             private PortableThreadPool()
  9.             {
  10.                 _minThreads = HasForcedMinThreads ? ForcedMinWorkerThreads : (short)Environment.ProcessorCount;
  11.                 _maxThreads = HasForcedMaxThreads ? ForcedMaxWorkerThreads : DefaultMaxWorkerThreadCount;
  12.                 _legacy_maxIOCompletionThreads = 1000;
  13.             }
  14.         }
  15.         public bool SetMinThreads(int workerThreads, int ioCompletionThreads)
  16.         {
  17.             if (workerThreads < 0 || ioCompletionThreads < 0)
  18.             {
  19.                 return false;
  20.             }
  21.             bool flag = false;
  22.             bool flag2 = false;
  23.             this._threadAdjustmentLock.Acquire();
  24.             if (workerThreads > (int)this._maxThreads)
  25.             {
  26.                 return false;
  27.             }
  28.             if (ioCompletionThreads > (int)this._legacy_maxIOCompletionThreads)
  29.             {
  30.                 return false;
  31.             }
  32.         }
复制代码
这个 Debugger.Break(); 发出的 int 3 信号,我们可以用 VS,DnSpy,WinDbg 如许的调试器去捕获,参考代码如下:
  1.         private static void GateThreadStart()
  2.         {
  3.             bool debuggerBreakOnWorkStarvation = AppContextConfigHelper.GetBooleanConfig("System.Threading.ThreadPool.DebugBreakOnWorkerStarvation", false);
  4.             while (counts.NumProcessingWork < threadPoolInstance._maxThreads && counts.NumProcessingWork >= counts.NumThreadsGoal)
  5.             {
  6.                 if (debuggerBreakOnWorkStarvation)
  7.                 {
  8.                     Debugger.Break();
  9.                 }
  10.             }
  11.         }
复制代码

三:总结

我们聊到了两种提升线程注入的方法,尤其是第二种让人意难平,面对上游洪水猛兽般的对线程池举行DDOS攻击,下游的线程不顾一切,倾家荡产的去承接,这是一种明知不可为而为之的悲壮之举。


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

耶耶耶耶耶

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表