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

来源:这里教程网 时间:2026-02-21 17:37:28 作者:

ForEachAsync 不是 .NET 原生 API

直接说结论:

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

ForEachAsync 通常怎么实现和使用

常见自定义

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

public static async Task ForEachAsync<T>(this IEnumerable<T> source, Func<T, Task> 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.Dataflow
ActionBlock<t></t>
异常行为:任意一个
Task
抛出异常,
Task.WhenAll
就会以
AggregateException
形式抛出,所有异常都会被捕获(不像串行
foreach + await
遇到第一个异常就停)

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

Parallel.ForEach
在每个线程上执行的是同步委托
Action<t></t>
,传入
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<t task></t>
的抽象(如
AsyncEnumerable
ForEachAwaitAsync
,.NET 6+ 的
IAsyncEnumerable<t>.ForEachAwaitAsync</t>
扩展),或者自己加信号量限流的
ForEachAsync
实现。

最易被忽略的一点:异步并发数 ≠ 线程数,而

Parallel.ForEach
的度量单位是线程。IO 操作不占线程,但可能占 socket、数据库连接、API 配额——这些才是
ForEachAsync
真正要管的资源。

相关推荐