商城首页欢迎来到中国正版软件门户

您的位置: 首页 > 文章列表 > 编程开发 > C#中Parallel.ForEachAsync的用法_C# .NET 6并行异步循环教程【最新】

C#中Parallel.ForEachAsync的用法_C# .NET 6并行异步循环教程【最新】

  发布于2026-07-06 阅读(0)

扫一扫,手机访问

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

C#中Parallel.ForEachAsync的用法_C# .NET 6并行异步循环教程【最新】

先澄清一个常见的误解:`Parallel.ForEachAsync` 并不等于 `Parallel.ForEach` 的异步平替。你不能直接把 `async` lambda 塞进去,否则看似正常,实则暗藏玄机。它专为 I/O 密集型异步任务设计,核心价值是可控并发度,不是“让 foreach 变快”。

为什么不能把 async/await 塞进 Parallel.ForEach

你可能会写 `Parallel.ForEach(items, async item => await DoAsync(item))`,编译通过,执行也看似正常。但编译器会静默地把这个 lambda 转成 `Action`,返回的 `Task` 被直接丢弃。结果就是:函数立即返回,异步操作在后台“裸奔”,主流程不等、不捕获异常、不保证完成——最常见的表现就是 `dicts.Count` 为 0,数据全丢了。

  • `Parallel.ForEach` 的 `body` 参数类型固定为 `Action`,不接受 `Func`
  • 即使编译通过,实际执行的是“启动异步操作后立刻结束本轮迭代”,不是真正等待
  • 没有内置取消传播、异常聚合或并发控制,极易漏错、丢数据、线程池饥饿

Parallel.ForEachAsync 必须用 IAsyncEnumerable 或显式转换

它只重载了两个签名:`ForEachAsync(IAsyncEnumerable, ...)` 和 `ForEachAsync(IEnumerable, ...)`。后者看似支持普通集合,但底层仍会调用 `.ToAsyncEnumerable()` 包装——这意味着:若你传 `List`,它不会“自动变异步”,而是按需枚举 + 异步调度。

  • 推荐显式构造 `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`。它不聚合结果,只保证所有迭代完成(或失败)。你需要自己在 `body` 中收集,且必须考虑并发写入安全。

  • 别用 `List.Add()` —— 非线程安全,会丢数据或抛 `InvalidOperationException`
  • 推荐 `ConcurrentBag`(无序、高性能)或 `ConcurrentQueue`(FIFO)
  • 若需保持原始顺序,得额外维护索引或用 `lock` + `List`,但会抵消部分并发收益
  • 注意 `CancellationToken` 传播:所有 `await` 调用都应传入 `ct`,否则取消可能不生效

最易被忽略的一点:它不处理异常聚合。任一迭代抛出未捕获异常,整个 `Task` 就以 `AggregateException` 失败,但你拿不到具体是哪个 `item` 出的问题——得在 `body` 里加 `try/catch` 单独记录,或用 `Task.WhenAll` + 手动限流来换更细粒度控制。

本文转载于:https://www.php.cn/faq/2444673.html 如有侵犯,请联系zhengruancom@outlook.com删除。
免责声明:正软商城发布此文仅为传递信息,不代表正软商城认同其观点或证实其描述。

热门关注