Parallel.ForEachAsync适合带并发度限制的逐项处理,核心是控制MaxDegreeOfParallelism;Task.WhenAll适合全量并发并聚合返回值,无内置限流。选型取决于是否需硬性限流、是否需返回值及数据量大小。
它本质是 foreach 的异步增强版,核心价值在于能控制最大并行数(MaxDegreeOfParallelism),避免瞬间拉起成百上千个 Task 压垮资源。比如调用外部 API、读写文件、数据库批量操作时,你通常不希望无节制并发。
常见错误是误以为它“一定比 Task.WhenAll 快”——其实它只是更可控;如果所有任务彼此完全独立且资源充足,Task.WhenAll 往往启动更快、调度开销更低。
IAsyncEnumerable 或可转为它的源(如 list.ToAsyncEnumerable())MaxDegreeOfParallelism 默认是 Environment.ProcessorCount,但对 IO 密集型任务常需手动设为 10–50 级别await Parallel.ForEachAsync(items, new ParallelOptions { MaxDegreeOfParallelism = 8 }, async (item, ct) =>
{
var result = await CallExternalApiAsync(item, ct);
// 注意:这里不能直接 return result
// 需要用 ConcurrentBag 或 lock 保护的 List
results.Add(result);
});
它只做一件事:把一堆 Task 同时启动,并等它们全部完成,最后返回 Task。没有内置并发数限制,也不关心执行顺序。
典型误用是拿它处理几千个 HTTP 请求却不加限流——可能触发连接池耗尽、远程服务限流或 SocketException。
IEnumerable> ,所以你要先用 Select 把数据映射成任务: items.Select(x => DoWorkAsync(x))
Task.WhenAll 就以 AggregateException 失败,需用 await task.ConfigureAwait(false) 或 try/catch 捕获var tasks = items.Select(item => CallExternalApiAsync(item)); var results = await Task.WhenAll(tasks); // results 是 T[]
不用背规则,现场问自己:
Parallel.ForEachAsync
CancellationToken)并让所有正在运行的任务及时退出?→ Parallel.ForEachAsync 对 ct 的传播更明确;Task.WhenAll 需确保每个子任务都正确接收并响应 ct
混合场景也常见:先用 Task.WhenAll 并发拉取一批 ID,再用 Parallel.ForEachAsync 分批次处理这些 ID——这时候两者不是互斥,而是分工。
Parallel.ForEachAsync 的 MaxDegreeOfParallelism 不是“最小并发数”,它只设上限;实际并发数取决于调度和等待 I/O 的时机,可能长期低于该值。
Task.WhenAll 的数组长度就是任务总数,但如果源数据量极大(比如 10 万条),直接生成 10

Task 会吃掉大量内存和调度开销——这时必须分块,用 Chunk + Task.WhenAll 或改用 Parallel.ForEachAsync。
两者都不自动处理重试、超时、降级;这些逻辑得你写在 CallExternalApiAsync 内部,而不是指望并行原语帮你兜底。