Parallel.ForEach仅在计算密集、单次迭代≥10ms、元素≥数千且无共享可变状态时才可能提速;轻量迭代、小集合、I/O操作、非线程安全写入或误用async均使其显著变慢。

Parallel.ForEach 不是 foreach 加个 Parallel 就能提速的魔法开关;它只在计算密集、单次迭代 ≥10ms、元素 ≥ 数千、且无共享可变状态时才可能带来收益。
Parallel.ForEach 什么时候会拖慢程序
常见现象:循环跑得比普通 foreach 还慢,CPU 占用高但吞吐没提升,甚至抛 InvalidOperationException 或结果不全。
- 单次迭代太轻——比如只是
Math.Abs(x)或简单属性赋值,线程调度开销远超计算本身 - 集合太小——
Enumerable.Range(0, 100)这类百级数据,分区+线程启动成本已占主导 - 误塞 I/O 操作——把
HttpClient.GetAsync()、File.ReadAllText()放进循环体,线程池迅速被占满,连接池耗尽,响应延迟飙升 - 往非线程安全集合写入——例如直接
results.Add(...)到普通List<T>,不是丢数据就是崩溃
正确写入共享结果的三种方式
不能靠 lock 包裹每次 Add——锁太细,性能崩盘;也不能指望“我加了锁就安全”,那是错觉。
- 用线程安全集合:
ConcurrentBag<T>(不保序)、ConcurrentQueue<T>(FIFO),适合只关心结果存在性、不依赖顺序的场景 - 用
localInit/localFinally做线程本地缓存:每个线程初始化自己的List<T>,处理完后在localFinally中一次性合并到全局ConcurrentBag<T> - 彻底放弃边算边存:改用 PLINQ——
data.AsParallel().Select(x => Process(x)).ToArray(),让 TPL 自动管理并行与聚合,代码更简洁、线程安全天然保障
Parallel.ForEach 和 async/await 根本不兼容
写成这样:Parallel.ForEach(data, async x => await ProcessAsync(x)),编译器直接报错:“无法将 Func<T, Task> 转换为 Action<T>”。
-
Parallel.ForEach只接受同步委托Action<T>,不支持异步语义 - I/O 密集型任务必须换路:用
Task.WhenAll(data.Select(x => ProcessAsync(x))),再配合SemaphoreSlim控制并发数(如限制最多 10 个 HTTP 请求同时发出) - 若真需要“异步外壳包着 CPU 密集计算”,只能外层套
Task.Run(() => Parallel.ForEach(...)),但这是极少数场景,多数时候说明设计有误
ParallelOptions.MaxDegreeOfParallelism 不是硬限流阀
设成 MaxDegreeOfParallelism = 4 并不等于“永远只跑 4 个线程”。它只是告诉 TPL “最多允许同时启动 4 个分区”,实际线程数还受线程池负载、CPU 核心数、以及数据分区大小影响。
- 对小集合(如 200 个元素),即使设了
MaxDegreeOfParallelism = 1,默认分区策略仍可能拆出多个小块,触发多线程 - 对 I/O 类任务,这个参数基本无效——因为真正瓶颈不在 CPU 分区,而在连接池或服务端限流
- 真正要强约束,并发控制必须下沉:I/O 用
SemaphoreSlim,CPU 密集且需精确控核用Partitioner.Create(..., true)配合自定义分区
最常被忽略的一点:Parallel 的性能收益高度依赖“任务粒度”和“数据规模”的匹配。拿 100 个毫秒级计算去并行,不如先检查是不是该用 PLINQ 或干脆别并行;而一旦决定上 Parallel,就绝不能绕过线程安全那关——哪怕只漏一个 Dictionary<K, V>.Add(),都可能让整个结果不可信。


















