task.run仅适用于cpu密集型任务且需限流,io密集型必须用原生async方法;并行等待应使用await task.whenall而非task.waitall;混合场景需拆分为await io + task.run cpu;避免parallel与async混用及task.factory.startnew误用。

别用 Task.WaitAll,也别在循环里无脑 Task.Run——这两招在真实服务里最容易引发线程池饥饿、响应延迟飙升或异常静默。
Task.Run 该用在哪儿、不该用在哪儿
它只适合 CPU 密集型任务,比如图像缩放、加密解密、大量数值计算。IO 密集型(如 HttpClient.GetAsync、File.ReadAllTextAsync)必须用原生 *Async 方法,包一层 Task.Run(() => File.ReadAllText()) 是伪异步,会占着线程池线程干等。
- CPU 密集:可用,但要限流,比如用
SemaphoreSlim控制并发数,避免开几百个把线程池拖垮 - IO 密集:禁用,查文档确认 API 是否有 *Async 版本,优先用它
- 混合场景(先读配置再校验):拆成两段,
await File.ReadAllTextAsync()+Task.Run(() => ValidateAndCompute()) - 常见错误现象:
ThreadPool.GetAvailableThreads返回 0、HTTP 请求超时陡增、日志里看不到异常但接口卡死
并行等待多个任务,必须用 await Task.WhenAll
Task.WaitAll 是同步阻塞调用,在 ASP.NET Core 或 WinForms 等有同步上下文的环境里,轻则性能归零,重则死锁。它返回 void,你还得手动遍历每个 task.Result,且一旦某个失败,整个调用就抛出 AggregateException,不拆包根本不知道哪个出错了。
-
await Task.WhenAll(task1, task2, task3)返回Task<tresult></tresult>,结果直接可取 - 异常处理要提前包裹单个 task:
var t1 = TryGetUserAsync().ContinueWith(t => t.Exception?.ToString() ?? "OK"),而不是等WhenAll完了再统一 try/catch - 数量大时(比如上千个请求),考虑分批,例如每 20–50 个一组
await Task.WhenAll(batch),避免调度器状态机开销过大 - 别传空集合给
Task.WhenAll:.NET 5 之前会返回未完成的Task,.NET 6+ 已修复,但旧项目仍需判空
Parallel 和 Task.WhenAll 别混用
Parallel.For 只接受 Action<int></int>,不支持 async 委托。你写 Parallel.For(0, n, async i => await LoadDataAsync(i)),编译器直接报错:委托类型不匹配。这不是语法糖问题,是设计隔离——Parallel 是同步并行模型,不感知 await。
- 硬要并行 + 异步?换路子:用
Task.WhenAll配合Enumerable.Range生成任务列表 -
Parallel.ForEach写入共享列表时结果错乱:因为List<t></t>不是线程安全的,Add可能同时修改内部数组和计数器;简单替换为ConcurrentBag<t></t>或用局部状态重载方案 -
ParallelOptions.MaxDegreeOfParallelism设成 1 并不等于普通 for 循环——它仍有分区逻辑、状态协调、异常包装等额外开销;真想串行,就别用Parallel - 并发爆炸导致
HttpRequestException: Too many open connections:默认不限并发数,HttpClient连接池有硬上限(通常 10–100),正确做法是用SemaphoreSlim包一层,或改用Parallel.ForEachAsync(.NET 6+)
Task.Factory.StartNew 是个深坑,除非你真懂
它默认不捕获同步上下文,也不适配 async lambda——写 Task.Factory.StartNew(async () => await DoWork()) 得到的是 Task<task></task>,不是 Task,await 它会卡住,异常还被吞掉。
- CPU 密集任务一律走
Task.Run:它内部做了正确封装,支持 async lambda 并自动Unwrap - 只有两种真实需求才考虑
Task.Factory.StartNew:需要TaskCreationOptions.LongRunning(告诉线程池“这活儿会长时间占用线程”),或需传入自定义TaskScheduler(如 WPF 的DispatcherSynchronizationContext) - 若要用,必须显式指定
TaskScheduler.Default,否则行为不可控 - 常见错误现象:任务看似启动了,但后续
await不返回;或者task.IsFaulted == true却查不到任何异常堆栈
真正难的不是写对语法,而是判断一个操作到底属于 CPU 密集还是 IO 密集——很多开发者把“看起来慢”的操作都当成 CPU 密集,结果用 Task.Run 把线程池压垮,而实际只是磁盘或网络在等。这点必须靠实测,不能凭感觉。











