为何Debug构建直接运行时线程池初始有2个线程导致程序挂起?
原本想探究线程池最大容量设为1时异步方法的运行机制,却碰到了异常:仅在Debug构建直接运行程序时,线程池初始就存在2个线程,导致ThreadPool无可用线程,程序无限挂起。但以下三种场景均无此问题:
- Debug构建调试运行
- Release构建调试运行
- Release构建直接运行
相关代码
public class Test { public static void Main() { int threadCount = ThreadPool.ThreadCount; Console.WriteLine($"thread count : {threadCount}"); ThreadPool.SetMinThreads(1, 2); Console.WriteLine(ThreadPool.SetMaxThreads(1, 2)); List<Task> tlist = new(); for (int i = 0; i < 3; i++) { Console.WriteLine($"repeat {i}"); tlist.Add(Task.Run(() => { threadCount = ThreadPool.ThreadCount; Console.WriteLine($"thread count : {threadCount}"); return FuncAsync(); })); Console.WriteLine($"repeat end {i}"); } Task.WaitAll(tlist.ToArray()); } public static async Task FuncAsync() { Console.WriteLine("FuncAsync"); } }
场景表现
| 构建模式 | 调试运行 | 直接运行 |
|---|---|---|
| Debug | 正常工作 | 无法运行 |
| Release | 正常工作 | 正常工作 |
异常成因
Debug构建的调试辅助线程注入
Debug构建直接运行时,.NET运行时会自动注入额外的线程池线程,用于支持调试相关的后台功能(比如诊断信息收集、代码追踪等),这就导致线程池初始线程数达到2个,超出了你设置的最大线程数1。此时线程池已无可用线程配额,新提交的任务只能进入等待队列。线程池调度与主线程阻塞的死锁
你通过Task.Run提交了3个任务,每个任务在执行异步方法前会占用一个线程池线程。当线程池无可用线程时,后续任务无法被调度执行;而主线程调用Task.WaitAll会一直阻塞,等待所有任务完成,最终形成死锁:等待队列的任务拿不到线程,主线程也无法继续推进。调试模式的线程池行为差异
当处于调试运行状态时(无论Debug还是Release构建),调试器会修改线程池的调度规则——比如不会让调试辅助线程占用正式的线程池配额,或者临时放宽线程创建限制,因此不会出现线程耗尽的情况;而Release构建直接运行时,运行时不会注入这些调试相关线程,线程池初始线程数与你设置的MinThreads一致,任务可以正常调度。异步方法的同步执行特性
虽然FuncAsync标记为异步,但它内部没有任何实际的异步等待操作,任务会以同步方式执行。即便如此,线程池初始线程数超限的问题依然会导致后续任务无法获取线程,最终引发挂起。
内容的提问来源于stack exchange,提问作者JaeEun Lee

