Loading... ## 线程池爬山算法 * `CLR`的线程池具有`最小线程数`和`最大线程数`的限制,默认`最小线程数`为CPU的逻辑处理器数量,当同时使用的线程数超过`最小线程数`后,它以适中的速率(例如每秒1或2个)创建新的线程。这意味着当你的线程数超过`最小线程数`后,超出部分的线程创建请求则需要进行排队,且越后面的线程等待时间越长。 * `CLR`设计默认线程池数量与CPU逻辑处理器数量相同是有原因的,两个好处: * 线程数过多的情况下,`CPU`需要在多个线程间进行切换,这会导致额外的线程切换开销,线程数越多则`CPU`大部分时间都花在了切换线程上下文上。 * 如果某个方法是阻塞的,那么大量的请求进来会导致线程无限制的被创建,会瞬间消耗大量的内存。 ## Practice * 我们可以简单的创建一个`NET Core Console`程序进行验证 ```csharp static void Main(string[] args) { var processThreads = 0; while (true) { Task.Factory.StartNew(() => { processThreads++; Console.WriteLine($"Currnet Thread Number:{processThreads} CurrentDateTime:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff")}"); Thread.Sleep(Timeout.Infinite); }); //防止创建过多的等待线程 Thread.Sleep(1); } } ``` 此程序每1毫秒创建一个新的线程去打印创建的线程数量以及时间信息。程序输出如下: ![file](https://wangshenjie.com/wp-content/uploads/2020/01/5e1e77a190fc0.png) 不难看出,前面8个线程是立即被创建的,当超过8个`Worker Thread`后,则会变成每1秒创建一个新线程。 如果我们在程序启动时设置最小线程数: ```csharp static void Main(string[] args) { //读取最小线程数 ThreadPool.GetMinThreads(out var _, out var minCompletionPortThreads); //设置最小线程数 ThreadPool.SetMinThreads(50, minCompletionPortThreads); var processThreads = 0; while (true) { Task.Factory.StartNew(() => { processThreads++; Console.WriteLine($"Currnet Thread Number:{processThreads} CurrentDateTime:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff")}"); Thread.Sleep(Timeout.Infinite); }); //防止创建过多的等待线程 Thread.Sleep(1); } } ``` ![file](https://wangshenjie.com/wp-content/uploads/2020/01/5e1e78f70e770.png) 可以看到前50个线程是立即被创建的,从第51个线程开始每1秒创建一个新线程。 ## `ASP.NET Core` & `Worker Threads` * `ASP.NET Core`的每次请求都会占用一个`Worker Thread`,如果我们的`最小线程数`为8且并发10个请求,那么有8个请求将被立即处理,剩下两个请求则会被`CLR`限制线程的创建。 * 如果在`ASP.NET Core`中的请求执行速度非常快,那么它将立即释放线程给后续请求使用,所以即使你上百的并发数量可能工作线程数量才十个甚至更少,但是如果你的请求时间很长,那么需要适当的设置`最小线程数`。 * 一般情况下,即使不设置`最小线程数`,当并发请求维持一定时间后不波动,线程池也能自动增长到能够维持并发的线程数量,但是在突然爆发的并发请求来临时,它将限制线程的创建速度。当并发突然停止时,`CLR`将维持这些线程在线程池中用于接下来的请求,但是当线程空闲时长超过一段时间后(可能是15秒),它们将被回收。 ## Workaround * 一个好的方式是采用异步编程,当处理耗时`I/O`绑定操作时,线程并未在此等待,当`I/O`操作完成时,线程再继续处理,这能够最大效率的使用线程(提高线程的利用率,减少等待情况)。 * 但是业务系统往往不能立即做出良好的更改,我们可以使用`ThreadPool.SetMinThreads()`在程序启动时来设置`最小线程数`,这通常不是一个好的解决方案,不过它确实有效(增加线程的数量)。 ## Best Practice * 对于`I/O`绑定的操作,使用异步编程的方式编写非阻塞代码。 * 对于`CPU`绑定的操作,使用`Task.Run`来运行多个任务。 ## Recommended Reading * [Diagnosing .NET Core ThreadPool Starvation with PerfView (Why my service is not saturating all cores or seems to stall)](https://docs.microsoft.com/en-us/archive/blogs/vancem/diagnosing-net-core-threadpool-starvation-with-perfview-why-my-service-is-not-saturating-all-cores-or-seems-to-stall) * [Thread Injection](https://docs.microsoft.com/en-us/previous-versions/msp-n-p/ff963549(v=pandp.10)?redirectedfrom=MSDN#thread-injection) * [The CLR Thread Pool 'Thread Injection' Algorithm](https://mattwarren.org/2017/04/13/The-CLR-Thread-Pool-Thread-Injection-Algorithm) ## Reference * [StackExchange.Redis Timeouts](https://stackexchange.github.io/StackExchange.Redis/Timeouts) * [ThreadPool Growth: Some Important Details](https://gist.github.com/JonCole/e65411214030f0d823cb) * [Dedicated thread or a Threadpool thread?](https://docs.microsoft.com/zh-cn/archive/blogs/pedram/dedicated-thread-or-a-threadpool-thread) * [IOCP threads - Clarification?](https://stackoverflow.com/questions/28690815/iocp-threads-clarification) * [Where does the thread pool get new threads from when its total available worker threads has reached zero?](https://stackoverflow.com/questions/37544816/where-does-the-thread-pool-get-new-threads-from-when-its-total-available-worker) * [Use a more dependable policy for thread pool thread injection](https://github.com/dotnet/coreclr/issues/1754) * [How do Completion Port Threads of the Thread Pool behave during async I/O in .NET / .NET Core?](https://stackoverflow.com/questions/58057749/how-do-completion-port-threads-of-the-thread-pool-behave-during-async-i-o-in-ne) [1]: https://wangshenjie.com/usr/uploads/2020/04/2971301615.jpg 最后修改:2020 年 09 月 22 日 © 允许规范转载 赞 3 如果觉得我的文章对你有用,请随意赞赏