C#中Parallel.ForEachAsync的用法_C# .NET 6并行异步循环教程【最新】
很多人以为 `Parallel.ForEachAsync` 就是 `Parallel.ForEach` 的异步版本,顺手就把 `async` lambda 往里塞——结果踩坑了。实际上,它专门为 I/O 密集型异步任务而设计,核心价值是可控并发度,而不是让 foreach 变快。关键点:不能把 as
很多人以为 `Parallel.ForEachAsync` 就是 `Parallel.ForEach` 的异步版本,顺手就把 `async` lambda 往里塞——结果踩坑了。实际上,它专门为 I/O 密集型异步任务而设计,核心价值是可控并发度,而不是让 foreach 变快。关键点:不能把 async lambda 用在 `Parallel.ForEach` 上,因为编译器会把它转成 `Action` 丢掉 `Task`;必须用 `IAsyncEnumerable` 或显式转换;`MaxDegreeOfParallelism` 控制的是并发异步任务数量,不是线程数;结果收集需要手动保证线程安全,而且异常不会自动聚合。

先澄清一个常见的误解:`Parallel.ForEachAsync` 并不等于 `Parallel.ForEach` 的异步平替。你不能直接把 `async` lambda 塞进去,否则看似正常,实则暗藏玄机。它专为 I/O 密集型异步任务设计,核心价值是可控并发度,不是“让 foreach 变快”。
为什么不能把 async/await 塞进 Parallel.ForEach
你可能会写 `Parallel.ForEach(items, async item => await DoAsync(item))`,编译通过,执行也看似正常。但编译器会静默地把这个 lambda 转成 `Action
- `Parallel.ForEach` 的 `body` 参数类型固定为 `Action
`,不接受 `Func ` - 即使编译通过,实际执行的是“启动异步操作后立刻结束本轮迭代”,不是真正等待
- 没有内置取消传播、异常聚合或并发控制,极易漏错、丢数据、线程池饥饿
Parallel.ForEachAsync 必须用 IAsyncEnumerable 或显式转换
它只重载了两个签名:`ForEachAsync
- 推荐显式构造 `IAsyncEnumerable
`:比如用 `AsyncEnumerable.Range(1, 100)` 或自定义 `yield return IAsyncEnumerable` - 传 `IEnumerable
` 时,`ForEachAsync` 内部会做一次同步枚举(无额外开销),但语义更清晰 - 别依赖“隐式转换”;若源是数据库查询或流式 API,优先走 `IAsyncEnumerable` 原生路径
MaxDegreeOfParallelism 控制的是“正在运行的 async 任务数”,不是线程数
这是和 `Parallel.ForEach` 最本质的区别:`Parallel.ForEach` 的 `MaxDegreeOfParallelism` 限制线程池租用数;而 `ForEachAsync` 的同名参数控制的是“同时处于 `await` 等待状态的任务上限”。对 HTTP 请求、文件读写这类 I/O 操作,这才是真正影响吞吐和稳定性的关键。
- 默认值是 `Environment.ProcessorCount`,对 I/O 场景通常过大;建议设为 `10–50`,视远程服务限流策略调整
- 设为 `1` 等价于串行 async/await,适合强顺序或令牌桶场景
- 它实现的是滑动窗口模型:一个任务完成,立刻补上新任务,始终保持 ≤ N 个活跃异步操作
- 不会因某次 `await` 时间长就卡住整个循环——别的任务照常推进
结果收集必须手动线程安全,且无法直接返回数组
`ForEachAsync` 返回 `Task`,不是 `Task
- 别用 `List
.Add()` —— 非线程安全,会丢数据或抛 `InvalidOperationException` - 推荐 `ConcurrentBag
`(无序、高性能)或 `ConcurrentQueue `(FIFO) - 若需保持原始顺序,得额外维护索引或用 `lock` + `List
`,但会抵消部分并发收益 - 注意 `CancellationToken` 传播:所有 `await` 调用都应传入 `ct`,否则取消可能不生效
最易被忽略的一点:它不处理异常聚合。任一迭代抛出未捕获异常,整个 `Task` 就以 `AggregateException` 失败,但你拿不到具体是哪个 `item` 出的问题——得在 `body` 里加 `try/catch` 单独记录,或用 `Task.WhenAll` + 手动限流来换更细粒度控制。


































