c# Parallel.ForEachAsync 的用法和 Task.WhenAll 的区别

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

Parallel.ForEachAsync 适合“带并发度限制的逐项处理”

它本质是

foreach
的异步增强版,核心价值在于能控制最大并行数(
MaxDegreeOfParallelism
),避免瞬间拉起成百上千个
Task
压垮资源。比如调用外部 API、读写文件、数据库批量操作时,你通常不希望无节制并发。

常见错误是误以为它“一定比

Task.WhenAll
快”——其实它只是更可控;如果所有任务彼此完全独立且资源充足,
Task.WhenAll
往往启动更快、调度开销更低。

必须传入
IAsyncEnumerable<t></t>
或可转为它的源(如
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<T> 或 lock 保护的 List<T>
    results.Add(result);
});

Task.WhenAll 适合“全量并发 + 收集返回值”

它只做一件事:把一堆

Task<t></t>
同时启动,并等它们全部完成,最后返回
Task<t></t>
。没有内置并发数限制,也不关心执行顺序。

典型误用是拿它处理几千个 HTTP 请求却不加限流——可能触发连接池耗尽、远程服务限流或

SocketException

输入是
IEnumerable<task>></task>
,所以你要先用
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
是否必须拿到每个操作的返回值,且数量不大(Task.WhenAll 是否要响应取消(
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
内部,而不是指望并行原语帮你兜底。

相关推荐