ASP.NET Core中I/O阻塞与线程池爬山
线程池爬山算法
CLR的线程池具有最小线程数和最大线程数的限制,默认最小线程数为CPU的逻辑处理器数量,当同时使用的线程数超过最小线程数后,它以适中的速率(例如每秒1或2个)创建新的线程。这意味着当你的线程数超过最小线程数后,超出部分的线程创建请求则需要进行排队,且越后面的线程等待时间越长。CLR设计默认线程池数量与CPU逻辑处理器数量相同是有原因的,两个好处:- 线程数过多的情况下,
CPU需要在多个线程间进行切换,这会导致额外的线程切换开销,线程数越多则CPU大部分时间都花在了切换线程上下文上。 - 如果某个方法是阻塞的,那么大量的请求进来会导致线程无限制的被创建,会瞬间消耗大量的内存。
- 线程数过多的情况下,
Practice
- 我们可以简单的创建一个
NET Core Console程序进行验证
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毫秒创建一个新的线程去打印创建的线程数量以及时间信息。程序输出如下:

不难看出,前面8个线程是立即被创建的,当超过8个Worker Thread后,则会变成每1秒创建一个新线程。
如果我们在程序启动时设置最小线程数:
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);
}
}

可以看到前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)
- Thread Injection
- The CLR Thread Pool 'Thread Injection' Algorithm
Reference
- StackExchange.Redis Timeouts
- ThreadPool Growth: Some Important Details
- Dedicated thread or a Threadpool thread?
- IOCP threads - Clarification?
- Where does the thread pool get new threads from when its total available worker threads has reached zero?
- Use a more dependable policy for thread pool thread injection
- How do Completion Port Threads of the Thread Pool behave during async I/O in .NET / .NET Core?