Task.Run适合CPU密集型任务,不适合IO密集型任务;IO应优先用async/await原生异步API;并行等待用await Task.WhenAll而非Task.WaitAll;避免滥用Task.Factory.StartNew;并发需用SemaphoreSlim等控制。

Task.Run 适合做什么,不适合做什么
直接用 Task.Run 并不等于“并行提速”,它只是把工作扔给线程池——如果任务本身是 CPU 密集型(比如大量计算、图像处理),它确实能利用多核;但如果是 IO 密集型(比如读文件、调 HTTP 接口),用 Task.Run 反而浪费线程资源,应该优先用 async/await + 原生异步 API(如 HttpClient.GetAsync、File.ReadAllBytesAsync)。
常见错误现象:Task.Run(() => File.ReadAllText("big.log")) 看似“异步”了,实则阻塞线程池线程,高并发时可能耗尽线程,引发 ThreadPool.GetAvailableThreads 返回 0 或响应延迟飙升。
- CPU 密集任务:用
Task.Run合理,但注意别无节制开太多(比如循环里每个都Task.Run) - IO 密集任务:查文档确认 API 是否有
*Async版本,优先用它,而不是包一层Task.Run - 混合场景(比如先读配置再计算):拆开,IO 部分用原生 async,CPU 部分再用
Task.Run
并行执行多个独立任务,别用 Task.WaitAll 就完事
Task.WaitAll 会同步阻塞当前线程,彻底失去异步优势;在 ASP.NET Core 这类上下文敏感环境里,还可能引发死锁或上下文切换开销。真正要等一组任务完成,应该用 await Task.WhenAll。
使用场景:批量调用多个微服务接口、同时解析多个 JSON 文件、预热多个缓存项。
参数差异:Task.WhenAll 返回 Task<TResult[]>,能直接拿到所有结果;Task.WaitAll 返回 void,还得手动从 Task 对象取 Result,且一旦某个失败,得自己遍历 IsFaulted 判断。
- 永远优先写
await Task.WhenAll(tasks),不是Task.WaitAll(tasks) - 如果某一个任务失败不能影响整体(即“尽力而为”),用
Task.WhenAll(tasks).ContinueWith(...)或单独try/catch包裹每个 task - 注意内存:1000 个
Task同时WhenAll,不会立刻创建 1000 个线程,但会占用调度器和状态机开销,必要时考虑分批(如每 50 个一批)
Task.Factory.StartNew 的坑比你想象的多
除非你明确需要控制 TaskCreationOptions(比如 LongRunning),否则别碰 Task.Factory.StartNew。它默认不捕获同步上下文,也不适配 async lambda——写 Task.Factory.StartNew(async () => await DoWork()) 会导致返回 Task<Task>,极易漏掉内层异常。
常见错误现象:任务看似执行了,但 await 它时卡住,或者异常被吞掉,日志里完全没痕迹。
- 替代方案:CPU 密集任务统一走
Task.Run;它内部做了正确封装,支持asynclambda 并自动展平Task<Task> - 真要用
StartNew?确保传入的是普通委托(() => { ... }),且显式指定TaskCreationOptions.DenyChildAttach防止子任务意外挂起父任务 -
LongRunning选项只适用于持续数秒以上的任务,短任务用它反而增加调度负担
并行度控制不是可选项,而是必选项
放任 Task.Run 或 Parallel.ForEach 自由并发,很容易打满 CPU 或压垮下游服务。C# 没有内置“最大并发数”开关,得自己控。
性能影响:不限制时,1000 个任务可能瞬间启动几百个线程,线程切换开销远超实际工作时间;兼容性上,.NET 6+ 的 ParallelOptions.MaxDegreeOfParallelism 在 Parallel.ForEach 中有效,但对 Task.Run 集合无效。
- 用
SemaphoreSlim控制Task.Run并发:先await semaphore.WaitAsync(),执行完semaphore.Release() -
Parallel.ForEach必须设new ParallelOptions { MaxDegreeOfParallelism = 4 },别依赖默认值(它是Environment.ProcessorCount) - HTTP 批量调用?用
HttpClient实例复用 +WhenAll+ 上面的信号量,三者缺一不可
最容易被忽略的是:异常发生后信号量没释放、或者 using var 写错位置导致 SemaphoreSlim 提前释放。这种 bug 不会报错,只会让并发数悄悄失控。









