parallel.foreachasync不能替代parallel.foreach,它是专为i/o密集型异步任务设计的独立调度器,基于task pump模型控制并发任务数而非线程数,需手动处理结果收集与异常,不支持线程本地状态。

Parallel.ForEachAsync 不能替代 Parallel.ForEach
它不是“给 Parallel.ForEach 加个 async”,而是专为 I/O 密集型异步任务设计的独立调度器。你传 async item => await DoAsync(item) 给 Parallel.ForEach,编译器会静默转成 Action<t></t>,丢弃返回的 Task——结果就是所有异步操作“裸奔”,主流程不等、不捕获异常、dicts.Count 为 0 就是典型表现。
真正要并行发 HTTP 请求或读文件,必须用 Parallel.ForEachAsync,而不是硬塞 async lambda 到旧 API 里。
必须传 IAsyncEnumerable 或显式构造
Parallel.ForEachAsync 只接受两种源:要么是 IAsyncEnumerable<t></t>,要么是 IEnumerable<t></t>(后者内部会调用 .ToAsyncEnumerable() 包装)。但别依赖隐式转换:
- 若源来自数据库查询(如 EF Core 的
AsAsyncEnumerable()),直接传过去,走原生路径 - 若源是普通
List<t></t>,推荐显式用AsyncEnumerable.FromEnumerable(list)或AsyncEnumerable.Range(1, n) - 手写
yield return的IAsyncEnumerable<t></t>更灵活,比如流式生成或分页拉取
传 IEnumerable<t></t> 不会“自动变快”,只是同步枚举一次;语义上不如明确构造 IAsyncEnumerable 清晰。
MaxDegreeOfParallelism 控制的是并发任务数,不是线程数
这是和 Parallel.ForEach 最本质的区别:
-
Parallel.ForEach的MaxDegreeOfParallelism限制线程池租用数 -
Parallel.ForEachAsync的同名参数控制“同时处于await等待状态的任务上限” - 对 HTTP、文件读写这类 I/O 操作,这才是影响吞吐与稳定性的关键
它的调度模型是滑动窗口:假设设为 3,有 6 个元素,执行过程是 A/B/C 启动 → C 完成后立即启动 D → B 完成后启动 E → A 完成后启动 F。任何时刻运行中任务 ≤ 3,永远“有空位就补”。
结果收集和异常处理必须手动处理
Parallel.ForEachAsync 不聚合异常,也不提供线程本地状态(localInit/localFinally)支持:
- 想收集结果?得自己用
ConcurrentBag<t></t>或ConcurrentQueue<t></t>,注意前者不保序 - 想按顺序收集?别边跑边存,改用
await Task.WhenAll(items.Select(x => ProcessAsync(x)))+ToArray() - 异常发生时,整个
Task会失败,但不会告诉你哪个 item 出错——需在body内部try/catch单独记录 - 取消传播靠
CancellationToken参数,但 body 里必须主动检查或传给底层异步调用(如HttpClient.GetAsync(uri, ct))
最易被忽略的一点:它内部是 Task Pump 模型,不是基于线程切分,所以别指望用 ThreadLocal<t></t> 或锁来优化共享状态——那是在对抗设计意图。











