应优先使用 Task.WhenAll 而非 Task.WaitAll:前者异步不阻塞线程,支持快速失败和高并发;后者同步阻塞当前线程,易致 UI 冻结、请求挂起或线程耗尽,仅适用于纯控制台、隔离线程或单元测试等安全场景。

别用 Task.WaitAll 做并发等待,除非你明确知道自己在阻塞哪个线程、且它不重要。 它的“性能”不是快慢问题,而是线程资源是否被无谓锁死——尤其在 UI 或 ASP.NET Core 请求线程上,Task.WaitAll 一调就卡死或死锁,根本没机会比。
Task.WaitAll 会直接吃掉当前线程,直到所有任务结束
它是个同步方法,调用后当前线程立刻停住,不做任何事,光等。哪怕你传进去的是 Task.Run 启动的后台任务,主线程也照卡不误。
常见错误现象:
- WinForms/WPF 界面冻结,按钮点不动,动画停摆
- ASP.NET Core 中间件里调用,整个 HTTP 请求挂起,超时返回 500
- 控制台程序看似正常,但线程池线程被占着,高并发下线程耗尽
使用场景仅限于:
- 纯控制台工具(无响应性要求)
- 后台服务中某个已明确隔离的线程(如
Thread.Start起的专用工作线程) - 单元测试里为简化逻辑而临时同步等待(但应优先用
await+Task.WhenAll)
Task.WhenAll 是真异步,并发执行不抢线程
它不阻塞,只打包一个 Task 返回,真正释放线程资源。配合 await,线程在等待期间可去处理其他请求或消息。
关键行为:
- 所有任务并行启动,不排队
- 返回的
Task在最后一个任务完成时才完成(不是最快那个) - 任一任务抛异常,
await Task.WhenAll(...)会直接 throw 第一个异常(AggregateException被解包),不会等其余任务 - 有返回值的任务(如
Task<int>),await Task.WhenAll(t1, t2)得到int[]
示例:
var tasks = new[] {
GetDataAsync("user"),
GetDataAsync("order"),
GetDataAsync("log")
};
int[] results = await Task.WhenAll(tasks); // 三路并发,结果按顺序进数组
WaitAll 和 WhenAll 的异常传播机制完全不同
这是最容易踩坑的地方:表面都是“等全部完成”,但出错时行为天差地别。
Task.WaitAll:
- 必须用
try/catch(AggregateException)捕获 - 即使第一个任务秒抛异常,它仍会等所有任务跑完才 throw(包括已失败的和还在跑的)
-
ex.InnerExceptions里能拿到全部失败原因,但时间成本已浪费
Task.WhenAll:
-
await后直接 throw 第一个异常(比如NullReferenceException),不用解包 - 其余任务继续运行(除非你主动
.Cancel())——这点常被忽略 - 若需收集全部异常,得手动检查每个 task 的
task.Exception属性,而不是靠 await
所以,当你要“快速失败+及时止损”,Task.WhenAll 更准;当你要“全量诊断+不差那几毫秒”,才考虑 WaitAll(且仅限安全线程)。
性能差异本质是线程模型差异,不是 CPU 时间
两者底层调度开销几乎一样,真正的性能分水岭在于:
-
WaitAll→ 占用一个线程单位时间(比如 2s),期间该线程无法复用 -
WhenAll→ 几乎零线程占用,2s 内该线程可处理上百个其他 async 操作
在 ASP.NET Core 中,这意味着:
- 100 个并发请求用
WaitAll→ 可能瞬间吃光默认 5000 线程池线程,触发队列堆积 - 同样 100 请求用
WhenAll→ 可能只用不到 10 个线程轻松扛住
别测单次耗时,去压测吞吐和延迟毛刺——WaitAll 的“慢”藏在连接池耗尽、GC 压力突增、线程饥饿这些地方。
最常被忽略的一点:Task.WhenAll 返回的 Task 本身不启动任何东西,它只是观察者;所有任务必须在传入前已 Start 或由 async 方法自动触发。漏掉这步,你会等一个永远不会完成的空壳任务。



















