应避免task.waitall和循环中滥用task.run,cpu密集型任务才适用task.run并需限流,io密集型应直接用原生async方法,混合场景需拆分处理,等待多任务必须用await task.whenall而非task.waitall。

别用 Task.WaitAll,也别在循环里无脑 Task.Run —— 这两类写法在真实服务中是最容易引发线程池饥饿、响应延迟飙升或异常静默的根源。
Task.Run 什么时候该用、什么时候不该用
关键看任务性质:CPU 密集型才适合扔给线程池;IO 密集型包一层 Task.Run 只是自欺欺人。
- CPU 密集型(如图像缩放、加密解密、大量数值计算):用
Task.Run合理,但要控制并发数,比如用SemaphoreSlim限流,避免开几百个任务把线程池拖垮 - IO 密集型(如
HttpClient.GetAsync、File.ReadAllTextAsync):直接调原生*Async方法,不要Task.Run(() => File.ReadAllText(...))—— 这种写法会占用线程池线程干等,不是异步,是“伪异步” - 混合场景(比如先读配置文件再做校验计算):拆成两段,
await File.ReadAllTextAsync(...)+Task.Run(() => ValidateAndCompute(...)) - 常见错误现象:
ThreadPool.GetAvailableThreads返回 0、HTTP 请求超时陡增、日志里看不到异常但接口卡死
并行等待多个任务:必须用 await Task.WhenAll
Task.WaitAll 是同步阻塞调用,在 ASP.NET Core 或 WinForms 等有同步上下文的环境里,轻则性能归零,重则死锁。
-
await Task.WhenAll(tasks)返回Task<tresult></tresult>,结果按传入顺序排列,可直接遍历 -
Task.WaitAll(tasks)返回void,你还得手动遍历每个task.Result,且一旦某个失败,整个调用就抛出AggregateException,不拆包根本不知道哪个出错了 - 异常处理要提前包裹单个 task:比如
var t1 = TryGetUserAsync().ContinueWith(t => t.Exception?.ToString() ?? "OK"),而不是等WhenAll完了再统一 try/catch - 数量大时(比如上千个请求),考虑分批,例如每 20–50 个一组
await Task.WhenAll(batch),避免调度器状态机开销过大
别碰 Task.Factory.StartNew,除非你真懂 TaskCreationOptions
它默认不捕获同步上下文,也不支持 async lambda 的自动展平 —— 写 Task.Factory.StartNew(async () => await DoWork()) 得到的是 Task<task></task>,不是 Task,await 它会卡住,异常还被吞掉。
- CPU 密集任务一律走
Task.Run:它内部做了正确封装,支持 async lambda 并自动Unwrap - 真需要
LongRunning或PreferFairness选项时,才考虑Task.Factory.StartNew,且必须显式指定TaskScheduler.Default - 常见错误现象:任务看似启动了,但后续 await 不返回;或者
task.IsFaulted == true却查不到任何异常堆栈
最常被忽略的一点:并行不等于更快。如果所有任务都在争抢同一个锁、共用一个数据库连接池、或反复触发 GC,加再多线程也白搭。先压测、再分析瓶颈,比盲目上 Task.Run 有用得多。











