一:背景
1. 講故事
前面二篇我們聊到了 Thread.Sleep
和 Task.Result
場景下的線程注入邏輯,在線程饑餓的情況下注入速度都不是很理想,那怎么辦呢?有沒有更快的注入速度,這篇作為 動態注入 的終結篇,我個人總結如下兩種方法,當然可能有更多的路子,知道的朋友可以在下面留言。
二:提高注入速度的兩種方法
1. 降低GateThread的延遲時間
上一篇跟大家聊過 Result 默認情況下GateThread每秒會注入4個,底層邏輯是由 Blocking.MaxDelayMs=250ms
變量控制的,言外之意就是能不能減少這個變量的值呢?當然可以的,這里我們改成 100ms,參考代碼如下:
static void Main(string[] args)
{
AppContext.SetData("System.Threading.ThreadPool.Blocking.MaxDelayMs", 100);
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();
}
現在我們還是用上一篇的方法在如下三個方法 HasBlockingAdjustmentDelayElapsed,PerformBlockingAdjustment,CreateWorkerThread
上埋日志斷點,埋好之后運行程序觀察。
從卦中的輸出結果看,注入速度明顯快了很多,判斷閾值也從 250ms 變成了 100ms,每秒能注入7~8
個線程,所以這是一個簡單粗暴的提速方法。
2. 提高 MinThreads 的閾值
看過上兩篇的朋友應該知道,我用過 噴涌而出
四個字來形容前 12個線程,這里的12是因為我的機器是 12 核,言外之意就是為什么要設置12呢?我能不能給它提升到 120,1200甚至更高的 12000 呢?這樣線程的注入速度不是更快嗎?有了這個想法趕緊上一段代碼,參考如下:
static void Main(string[] args)
{
ThreadPool.SetMinThreads(10000, 10);
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();
}
從卦中看,直接秒了這個 10000 個任務,但不要忘了你的程序此時有1w個線程,如果是32bit程序大概率因為虛擬地址不足直接崩了,如果是 64bit 可能也會導致非常可觀的內存占用。
有些人可能對底層邏輯感興趣,我特意花了點時間繪了一張圖來描述底層的運轉邏輯。
之所以能快速的產生新線程,核心判斷條件是 numProcessingWork <= counts.NumThreadsGoal
,我們設置的 MinThread=10000 最后給到了 NumThreadsGoal 字段,所以現有線程數不超過 10000 的話,就會不斷的調用 CreateWorkThread
產生新的工作線程。
接下來我們再聊一下 SetMinThreads 這里面的坑吧,如果你將剛才的 ThreadPool.SetMinThreads(10000, 10);
改成 ThreadPool.SetMinThreads(10000, 10000);
的話,將不會有任何效果,截圖如下:
為什么會出現這樣的情況呢?這得從源碼上找答案,參考代碼如下:
public class PortableThreadPool
{
private short _minThreads;
private short _maxThreads;
private short _legacy_maxIOCompletionThreads;
private const short DefaultMaxWorkerThreadCount = MaxPossibleThreadCount;
private const short MaxPossibleThreadCount = short.MaxValue;
private PortableThreadPool()
{
_minThreads = HasForcedMinThreads ? ForcedMinWorkerThreads : (short)Environment.ProcessorCount;
_maxThreads = HasForcedMaxThreads ? ForcedMaxWorkerThreads : DefaultMaxWorkerThreadCount;
_legacy_maxIOCompletionThreads = 1000;
}
}
public bool SetMinThreads(int workerThreads, int ioCompletionThreads)
{
if (workerThreads < 0 || ioCompletionThreads < 0)
{
return false;
}
bool flag = false;
bool flag2 = false;
this._threadAdjustmentLock.Acquire();
if (workerThreads > (int)this._maxThreads)
{
return false;
}
if (ioCompletionThreads > (int)this._legacy_maxIOCompletionThreads)
{
return false;
}
}
從卦中代碼可以看到 ioCompletionThreads 默認最大值為 1000,如果你設置的值大于 1000 的話,那前面的 workerThreads 等于白設置了。。。這就很無語了。。。 如果參數有誤,你完全可以拋出一個異常來告訴我,,,而不是偷偷的掩埋錯誤信息,導致程序出現了我意想不到的行為。。。
為了湊篇幅,我再說一個有意思的參數 DebugBreakOnWorkerStarvation,它可以用來捕獲 線程饑餓
的第一現場,底層邏輯是C#團隊在代碼里埋了一個鉤子,參考如下:
private static void GateThreadStart()
{
bool debuggerBreakOnWorkStarvation = AppContextConfigHelper.GetBooleanConfig("System.Threading.ThreadPool.DebugBreakOnWorkerStarvation", false);
while (counts.NumProcessingWork < threadPoolInstance._maxThreads && counts.NumProcessingWork >= counts.NumThreadsGoal)
{
if (debuggerBreakOnWorkStarvation)
{
Debugger.Break();
}
}
}
這個 Debugger.Break();
發出的 int 3 信號,我們可以用 VS,DnSpy,WinDbg 這樣的調試器去捕獲,參考代碼如下:
static void Main(string[] args)
{
AppContext.SetSwitch("System.Threading.ThreadPool.DebugBreakOnWorkerStarvation", true);
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();
}
三:總結
我們聊到了兩種提升線程注入的方法,尤其是第二種讓人意難平,面對上游洪水猛獸般的對線程池進行DDOS攻擊,下游的線程不顧一切,傾家蕩產的去承接,這是一種明知不可為而為之的悲壯之舉。
轉自https://www.cnblogs.com/huangxincheng/p/18630175?
該文章在 2025/1/3 9:50:48 編輯過