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真正要管的资源。
