应优先使用 Task.WhenAll 而非 Task.WaitAll:前者异步不阻塞线程,支持快速失败和高并发;后者同步阻塞当前线程,易致 UI 冻结、请求挂起或线程耗尽。

别用 Task.WaitAll,也别在循环里无脑 Task.Run——这两类写法在真实服务中是最容易引发线程池饥饿、响应延迟飙升或异常静默的根源。
什么时候该用 Task.Run,什么时候必须绕开它
CPU 密集型任务(如图像缩放、加密解密、数值计算)才适合交给线程池;IO 密集型操作(如 HttpClient.GetAsync、File.ReadAllTextAsync)直接调原生 *Async 方法就行。
- 用
Task.Run(() => File.ReadAllText("a.txt"))是伪异步:它把 IO 等待塞进线程池线程里干等,白白占用线程,还可能拖垮ThreadPool.GetAvailableThreads到 0 - 正确写法是
await File.ReadAllTextAsync("a.txt"),底层走的是 IOCP,不占线程池线程 - 混合场景(比如先读配置再校验)要拆开:前段
awaitIO,后段Task.Run(() => Validate()),且记得用SemaphoreSlim限流,避免并发失控
Task.WhenAll 和 Task.WaitAll 的本质区别
Task.WhenAll 是异步等待,返回 Task<TResult[]>,结果顺序与传入一致;Task.WaitAll 是同步阻塞,返回 void,且一旦某个子任务失败,就抛出 AggregateException,不手动遍历 .InnerExceptions 根本看不出哪个出错了。
- ASP.NET Core 或 WinForms 中调
Task.WaitAll极易死锁,因为会抢同步上下文 - 想捕获单个任务异常?提前在每个 task 上加
.ContinueWith(t => t.Exception?.ToString() ?? "OK"),而不是等WhenAll完了再 try/catch - 上千个任务别一股脑
WhenAll,分批处理(比如每 20–50 个一组),否则调度器状态机开销会明显上升
为什么 Task.Factory.StartNew 多数时候该被放弃
它默认不捕获同步上下文,也不支持 async lambda 自动展平。写 Task.Factory.StartNew(async () => await DoWork()) 得到的是 Task<Task>,不是 Task,await 它会卡住,异常还可能被吞掉。
-
Task.Run内部做了正确封装,支持 async lambda 并自动Unwrap,日常开发够用 - 真需要
LongRunning或PreferFairness选项时,才考虑Task.Factory.StartNew,且必须显式指定TaskScheduler.Default - 绝大多数业务代码里,
Task.Run+await Task.WhenAll就是黄金组合
最容易被忽略的点是:任务启动时机和等待方式其实是两个独立决策。调用 async 方法那一刻任务就开始执行了,await 只是挂起当前方法去等——这个认知偏差会导致大量“以为并行实则串行”的 bug。



















