parallel.foreach 仅在单次迭代≥10ms且元素≥数千时才可能提升性能;否则线程调度开销反而拖慢速度,如对100个元素使用即属误用。

Parallel.ForEach 什么时候该用、什么时候不该用
它只在单次迭代 ≥ 10ms 且元素 ≥ 数千时才可能带来收益;否则线程调度开销反而拖慢整体速度。比如对 Enumerable.Range(0, 100) 调用 Parallel.ForEach,基本等于给小任务硬套大引擎。
常见误判场景:
- 把
HttpClient.GetAsync塞进循环体——这是 I/O 操作,应改用Task.WhenAll+ 异步方法 - 往同一个
List<t></t>里反复Add——会丢数据或抛InvalidOperationException - 循环体里修改共享字典的同一 key——结果不可预测,且无锁保护时大概率崩溃
Parallel.ForEach 写入共享集合的三种安全写法
不能直接在 body 里操作普通集合,必须隔离写入路径。最常用的是以下三种:
- 用线程安全集合:如
ConcurrentBag<t></t>(不保序)、ConcurrentQueue<t></t>(FIFO),适合只关心结果存在性、不依赖顺序的场景 - 用
localInit/localFinally做线程本地缓存:每个线程初始化自己的List<t></t>,处理完后在localFinally中合并到全局ConcurrentBag<t></t> - 彻底放弃边算边存:先用
AsParallel().Select(...).ToArray()得到结果数组,再统一处理——PLINQ 更适合这类链式转换
Task.WhenAll 和 Parallel.ForEach 混用会编译失败
Parallel.ForEach 只接受同步委托(Action<t></t>),而 async 方法返回的是 Task,类型根本不匹配。写成这样:Parallel.ForEach(data, async x => await ProcessAsync(x)),编译器直接报错“无法将‘Func
正确替代路径只有两条:
- I/O 密集型:用
Task.WhenAll(data.Select(x => ProcessAsync(x))),再加SemaphoreSlim控并发数(避免连接池耗尽) - CPU 密集型 + 需要异步外壳:用
Task.Run(() => Parallel.ForEach(...)),但仅限你明确知道外层需异步等待、且内层确实是纯计算
ParallelOptions.MaxDegreeOfParallelism 不是并发数上限的保险丝
设成 4 并不意味着永远只跑 4 个线程——它只是告诉 TPL “最多允许同时启动 4 个分区”,但实际线程数还受 ThreadPool 当前负载、系统 CPU 核心数、以及分区大小影响。尤其在小集合上,即使设了 MaxDegreeOfParallelism = 1,也仍可能触发多线程(因为默认分区策略会拆出多个小块)。
真正需要强约束时,得结合其他手段:
- 对 I/O 类任务,
MaxDegreeOfParallelism几乎无效,必须用SemaphoreSlim或Parallel.ForEachAsync(.NET 6+) - 想精确控制线程生命周期(比如每个线程配一个独立
HttpClient),只能用四参数重载的localInit/localFinally - 别指望靠它防住死锁或资源争用——它不解决共享状态问题,只管调度粒度











