c# ForAll 和 ForEach 在PLINQ中的区别

16次阅读

ForAll是Plinq专属的无返回、不保序、不合并结果的并行消费方法,仅用于ParallelQuery末端;foreach是IEnumerable/List的顺序遍历方法,ParallelQuery上不存在该方法。

c# ForAll 和 ForEach 在PLINQ中的区别

ForAll 是 PLINQ 专属的并行消费方法,ForEach 是普通集合的顺序遍历

ForAll 只存在于 ParallelQuery(即调用 AsParallel() 后的查询结果),它不返回值、不保证执行顺序、也不合并结果——每个线程拿到自己的数据块后立刻执行委托,完事就退出。ForEach 则是 IEnumerableList 上的实例方法,纯顺序执行,线程安全需自行保障,且会等全部元素处理完才返回。

  • ForAll 不能链式返回新集合,只适合“发通知”“写日志”“更新非共享状态”这类无返回、无依赖的操作
  • ForEach 在 PLINQ 中根本不存在——你写 list.AsParallel().ForEach(...) 会编译失败,因为 ParallelQuery 没有这个方法;真正能用的是 Parallel.ForEach(...)(来自 System.Threading.Tasks.Parallel),但那是另一套 API,和 LINQ 风格无关
  • 别把 Parallel.ForEachParallelQuery.ForAll 混为一谈:前者接受 IEnumerable 或分区器,后者只接受 ParallelQuery

为什么 ForAll 不保证顺序?这和 PLINQ 的分区机制直接相关

PLINQ 把源集合切分成若干段(partition),分给不同线程处理。这些段大小不固定、分配时机不确定、完成时间也不同。ForAll 就是让每个线程在自己分到的那块数据上“立刻开干”,不做任何等待或排序协调——所以输出顺序完全不可预测。

  • 如果你需要顺序输出(比如写入文件、生成有序报告),ForAll 不适用;该用 foreach 遍历 ToArray()ToList() 结果
  • ForAll 内部跳过结果合并步骤,因此比 ToArray() + foreach 快,尤其在数据量大、操作耗时长时优势明显
  • 若委托里访问了共享变量(如静态计数器、全局 list),必须加锁或改用线程安全类型(如 ConcurrentBag),否则结果错乱

常见误用:想并行又想要顺序,结果既慢又错

典型错误是这样写:

numbers.AsParallel()     .Where(n => IsPrime(n))     .OrderBy(n => n) // 强制全缓冲 + 排序合并     .ForAll(console.WriteLine); // 以为能按序打印素数

问题在于:OrderBy 会让 PLINQ 缓冲所有结果再排序,彻底抵消并行优势;而 ForAll 仍不保证输出顺序(即使输入已排序,多线程并发写控制台也会乱序)。

  • 要顺序输出:去掉 ForAll,改用 foreach (var x in query.OrderBy(...)) Console.WriteLine(x);
  • 要纯并行处理 + 丢弃结果:保留 ForAll,但删掉 OrderBy 等强制合并的运算符
  • 想边算边处理?PLINQ 默认“部分缓冲”,可用 WithMergeOptions(ParallelMergeOptions.NotBuffered) 让结果更早流出,但仍不保序

ForEach 方法名重复导致的认知陷阱

名字都叫 ForEach,但实际是三个不同东西:

  • List.ForEach():实例方法,顺序,单线程,属于 .net Framework 2.0 就有的老 API
  • Parallel.ForEach():静态方法,接受 IEnumerable 或自定义分区器,可配置并行度、取消令牌等,属于 TPL
  • ParallelQuery.ForAll():扩展方法,仅用于 PLINQ 查询链末端,无返回、无合并、不保序

它们之间没有继承或重载关系,只是命名巧合。选哪个,取决于你手头的数据类型和目标:是已有集合想并行遍历?用 Parallel.ForEach;是 LINQ 查询想加速过滤+消费?用 AsParallel().Where(...).ForAll(...);只是简单循环打印?foreach 最稳。

text=ZqhQzanResources