Parallel.ForEach 并非总能提速,轻量操作反因调度开销变慢;适用场景为单次迭代≥几毫秒且无强依赖,需避免共享变量、控制并发度、慎用异常与阻塞调用。
Parallel.ForEach 会自动分配线程,但不等于自动变快
多数人以为只要换成 parallel.foreach 就能提速,结果反而更慢——尤其当循环体里是轻量操作(比如简单数学计算或字符串拼接)时,线程调度和同步开销可能远超收益。
真正适合并行的场景是:单次迭代耗时 ≥ 几毫秒、且彼此无强依赖。例如读写文件、调用外部 API、图像像素处理。
- 用
Stopwatch实测单次body执行时间,低于 1ms 就别硬上Parallel.ForEach - 避免在循环体内访问共享变量;真要写,用
Interlocked.Increment或ConcurrentBag<T>,别直接 ++ 普通int - 默认线程数由
ThreadPool管理,但 CPU 密集型任务建议限制并发度:ParallelOptions.MaxDegreeOfParallelism = Environment.ProcessorCount
Parallel.For 的索引安全边界容易被忽略
Parallel.For 看似和普通 for 一样用,但它的迭代顺序不保证,而且多个线程可能同时进入同一段逻辑——如果你在循环里用了 list[i] = …… 这种写法,而 list 不是线程安全容器,就会出现覆盖或 IndexOutOfRangeException。
- 只对数组 / 列表做只读访问?没问题。写入?必须确认目标集合支持并发写,或改用
Parallel.ForEach+ConcurrentDictionary等结构 - 别在
Parallel.For里调用Thread.Sleep或阻塞 I/O,这会让线程池饥饿,拖慢整个程序 - 异常处理要小心:
AggregateException是默认包装器,ex.InnerExceptions才是你真正要 inspect 的错误列表
Parallel.Invoke 不适合串行逻辑拆分
看到“多个任务”,第一反应用 Parallel.Invoke 把几个方法包进去?这只有在它们完全独立、执行时间接近、且都属于 CPU 密集型时才合理。否则极易造成负载不均:一个任务跑 500ms,另两个 20ms,最后等那一个拖完才返回。
- 如果任务耗时不均,优先考虑
Task.Run+await Task.WhenAll,它不抢线程池资源,也更容易 await 和 cancel -
Parallel.Invoke内部没有超时机制,出问题只能靠外部CancellationToken中断,但中断后状态难清理 - 参数是
Action[],传 lambda 时注意闭包捕获的变量是否线程安全;尤其别在循环里写Parallel.Invoke(() => Do(x)),x 很可能被所有任务共享
别忘了 Parallel LINQ(PLINQ)这个更自然的入口
如果你已经在用 LINQ 查询集合,加个 AsParallel() 比硬套 Parallel.ForEach 更直观,而且 PLINQ 自动处理分区、合并、异常聚合。
- 不是所有 LINQ 操作都支持并行加速:
OrderBy、GroupBy效果明显;First、Any可能提前退出,反而比串行慢 - 用
WithDegreeOfParallelism(n)控制并发数,避免默认值在高核机器上开太多线程 - PLINQ 默认保持顺序(
AsOrdered()),如不需要,显式调用AsUnordered()能提升 10–20% 吞吐
多核不是银弹,Parallel 类只是工具箱里的一把扳手——拧得越用力,越要看清螺纹方向。实际项目里,先 profile 瓶颈点,再决定是换算法、加缓存,还是真需要并行。盲目套 Parallel,常常是给 GC 和线程调度添乱。