信息发布→ 登录 注册 退出

c# ForEachAsync 的用法和 Parallel.ForEach 的区别

发布时间:2026-01-10

点击量:
ForEachAsync 不是 .NET 原生 API,不存在于 System.Collections.Generic 或 System.Linq 中,而是开发者自定义或第三方库提供的异步遍历方法,基于 Task.WhenAll 实现并发执行,需注意限流与异常聚合;Parallel.ForEach 是 .NET 内置同步并行工具,不支持直接 await 异步操作。

ForEachAsync 不是 .NET 原生 API

直接说结论:ForEachAsync 不存在于 System.Collections.GenericSystem.Linq 中。它常被误认为是 List 的扩展方法,实际是开发者自己写的异步遍历辅助方法,或来自第三方库(如 Microsoft.VisualStudio.Threading 或社区 NuGet 包)。Parallel.ForEach 则是 .NET Framework 4+ 内置的并行同步执行工具,位于 System.Threading.Tasks 命名空间。

ForEachAsync 通常怎么实现和使用

常见自定义 ForEachAsync 是基于 Task.WhenAll 的并发控制,不是串行 await 每一项(那叫 foreach + await),而是批量触发所有异步操作再统一等待:

public static async Task ForEachAsync(this IEnumerable source, Func body)
{
    if (source == null) throw new ArgumentNullException(nameof(source));
    if (body == null) throw new ArgumentNullException(nameof(body));

    var tasks = source.Select(item => body(item));
    await Task.WhenAll(tasks);
}

使用时注意:

  • ForEachAsync 默认不控制并发数,1000 个元素就并发 1000 个 Task,可能压垮服务或耗尽连接池
  • 若需限流,得改用 SemaphoreSlim 包裹 body,或借助 System.Threading.Tasks.DataflowActionBlock
  • 异常行为:任意一个 Task 抛出异常,Task.WhenAll 就会以 AggregateException 形式抛出,所有异常都会被捕获(不像串行 foreach + await 遇到第一个异常就停)

Parallel.ForEach 是同步阻塞式并行,不能直接 await 异步操作

Parallel.ForEach 在每个线程上执行的是同步委托 Action,传入 async lambda 会导致“火把式异步”(fire-and-forget)——编译能过,但实际只启动了 Task 并立即返回,Parallel 不等它完成就继续下一项,最终结果不可控:

Parallel.ForEach(items, item =>
{
    SomeAsyncOperation(item).Wait(); // ❌ 不推荐:阻塞线程,易死锁、拖慢吞吐
});

正确做法只有两个:

  • 坚持同步逻辑:所有操作必须是 CPU-bound 或已同步封装(如 File.ReadAllBytes
  • 改用异步方案:放弃 Parallel.ForEach,回到 ForEachAsync(自定义或第三方)或 Task.WhenAll + Select

性能差异明显:Parallel.ForEach 适合密集计算;ForEachAsync 适合 I/O 密集(HTTP 请求、DB 查询),但必须小心资源竞争与并发上限。

别混淆 Task.Run + ForEachAsync 和 Parallel.ForEach

有人试图用 Task.Run(() => Parallel.ForEach(...)) 把同步并行“包一层”变成异步,这是典型误解:

  • 没解决根本问题:内部仍是同步执行,只是挪到了后台线程池线程上
  • 额外增加调度开销,且无法取消、难以监控进度
  • 如果 Parallel.ForEach 里混了 await,一样会掉进“未等待异步任务”的陷阱

真正需要异步并行时,优先选明确支持 Func 的抽象(如 AsyncEnumerableForEachAwaitAsync,.NET 6+ 的 IAsyncEnumerable.ForEachAwaitAsync 扩展),或者自己加信号量限流的 ForEachAsync 实现。

最易被忽略的一点:异步并发数 ≠ 线程数,而 Parallel.ForEach 的度量单位是线程。IO 操作不占线程,但可能占 socket、数据库连接、API 配额——这些才是 ForEachAsync 真正要管的资源。

标签:# 并发  # 第一个  # 这是  # 信号量  # 的是  # 抛出  # 死锁  # 不存在  # 遍历  # 第三方  # 自定义  # linq  # http  # 数据库  # visualstudio  # 异步  # 工具  # 线程  # Generic  # 委托  # Lambda  # select  # 封装  # 命名空间  # foreach  # gate  # .net  # c#  # 区别  # 异步任务  # microsoft  # ai  
在线客服
服务热线

服务热线

4008888355

微信咨询
二维码
返回顶部
×二维码

截屏,微信识别二维码

打开微信

微信号已复制,请打开微信添加咨询详情!