.NET 4.7同步封装异步代码的TPL并行调度异常问题咨询
问题分析与解答
你的问题核心都是**.NET Framework线程池的工作线程调度机制**,以及同步阻塞异步代码引发的线程饥饿导致的,下面分别解释:
1. 为什么Work方法要等到所有任务启动后才执行?
你的代码里存在多层线程池线程的阻塞链,直接导致了线程池资源耗尽和任务排队:
Task.Run(() => WorkSync(n))会占用一个线程池工作线程来执行WorkSync。- 在
WorkSync中,你用.GetAwaiter().GetResult()同步阻塞当前线程,等待WorkAsync完成——这会把这个线程池线程死死占住,直到WorkAsync结束。 WorkAsync里又调用Task.Run(() => Work(i)),这需要再申请一个线程池工作线程来执行Work。
当你循环启动15个任务时,很快就会耗尽线程池的初始工作线程(.NET Framework默认初始工作线程数等于CPU核心数,比如4核就是4个,8核就是8个)。此时,所有已启动的WorkSync线程都在阻塞等待,而Work的任务因为拿不到线程,只能在线程池的任务队列里排队。
线程池不会立刻创建新线程,它有一个线程注入延迟策略:当队列中有等待任务,但当前工作线程数已达初始最小值时,线程池会每隔约500ms才会创建一个新工作线程。这个过程需要等所有WorkSync任务都排队启动完毕(你的循环里每次Sleep 1ms,前几个快速启动,后面的要等线程池创建新线程才能启动),所以最终看起来Work是在所有任务启动后才开始执行。
2. 前10个任务启动极快,后5个却启动极慢?
这同样是线程池的线程注入机制导致的:
- 前几个任务(比如0-7,对应8核CPU的初始线程数)可以直接拿到线程池的初始工作线程,所以启动速度极快。
- 当初始工作线程被耗尽后,新的
Task.Run任务(第8个及以后)需要等待线程池创建新的工作线程。线程池的线程注入不是即时的,而是每隔一段时间(约500ms)才会创建一个,再加上前面的WorkSync线程一直阻塞占用着已有的线程,线程池只能慢慢注入新线程来处理后续的Task.Run请求。 - 你看到的后续任务间隔约1秒启动,是因为线程池创建新线程的延迟,加上每个新线程被创建后立刻被
WorkSync的阻塞占用,导致下一个任务还要继续等下一个新线程的创建。
优化建议:避免同步阻塞异步代码
要解决这个问题,核心是不要用同步方式阻塞异步代码,改用全异步的写法,让线程池线程可以被复用,而不是被死死占住:
class Program { static async Task Main(string[] args) // .NET 4.7支持异步Main { var tasks = new List<Task>(); for (int i = 0; i < 15; i++) { var n = i; tasks.Add(WorkAsyncWrapper(n)); await Task.Delay(TimeSpan.FromMilliseconds(1)); // 用Task.Delay代替Thread.Sleep,不阻塞线程 } await Task.WhenAll(tasks); } private static async Task WorkAsyncWrapper(int i) { Debug.WriteLine($"{i:000}\t{DateTime.Now:HH:mm:ss.fff}\tStartA"); await WorkAsync(i); // 异步等待,释放线程池线程 Debug.WriteLine($"{i:000}\t{DateTime.Now:HH:mm:ss.fff}\tFinishA"); } private static async Task WorkAsync(int i) { Debug.WriteLine($"{i:000}\t{DateTime.Now:HH:mm:ss.fff}\tStartB"); await Task.Run(() => Work(i)); Debug.WriteLine($"{i:000}\t{DateTime.Now:HH:mm:ss.fff}\tFinishB"); } private static void Work(int i) { Debug.WriteLine($"{i:000}\t{DateTime.Now:HH:mm:ss.fff}\tDo Something"); } }
修改后,线程池线程不会被阻塞,Work任务可以及时执行,所有任务的启动也不会有明显延迟。
内容的提问来源于stack exchange,提问作者vernou
相关产品推荐
相关产品推荐

