多数场景优先选Parallel.ForEach;Parallel.For仅适用于需明确索引的整数范围迭代;安全共享变量需用线程安全方式如localInit/localFinally、ConcurrentBag或PLINQ;IO密集型操作应改用async/await+Task.WhenAll。

Parallel.For 和 Parallel.ForEach 用哪个?
多数场景下优先选 Parallel.ForEach,尤其处理集合(IEnumerable、List、数组);Parallel.For 仅适用于需要明确索引控制的整数范围迭代(比如 for (int i = 0; i 的并行等价)。误用 Parallel.For 去遍历集合会多写索引安全检查,还容易引发 IndexOutOfRangeException。
常见错误现象:用 Parallel.For(0, list.Count, i => DoSomething(list[i])),但 list 被其他线程修改导致越界或数据错乱。
- 若需访问元素索引且集合不变,可用
Parallel.ForEach(list.AsParallel().Select((item, idx) => new { Item = item, Index = idx }), ...),但更推荐直接用AsParallel()+ LINQ -
Parallel.ForEach内部自动处理分区(Partitioning),对大集合性能更稳;Parallel.For分区策略较简单,高并发下负载可能不均 - 两者都默认使用
ThreadPool线程,不创建新线程,但会抢占 ThreadPool 工作项——长时间阻塞操作(如同步 IO)会拖慢整个应用
如何安全共享变量或收集结果?
直接在 Parallel.ForEach 或 Parallel.For 的 body 中读写普通变量(如 int sum = 0;)必然出错:多个线程同时 sum += x 会导致丢失更新。必须用线程安全方式。
推荐方案按场景分:
- 累加数值 → 用
Parallel.ForEach的重载支持localInit/localFinally,例如:int total = Parallel.ForEach(source, () => 0, (item, state, localSum) => localSum + item, localSum => Interlocked.Add(ref total, localSum));
- 收集对象列表 → 用
ConcurrentBag或ConcurrentQueue,避免锁竞争;别用List+lock,吞吐量差 - 需保持顺序(如日志写入)→ 改用
PLINQ的AsOrdered(),但注意性能损耗;或先并行处理再用Array.Sort后续排序
什么时候该换 PLINQ 而不是 Parallel 类?
当你操作的是集合、且逻辑以「转换→过滤→聚合」为主时,AsParallel() 更简洁安全。例如:source.AsParallel().Where(x => ExpensiveCheck(x)).Select(x => Transform(x)).Sum()。
Parallel 类无法自然表达链式数据流,硬写会拆成多层循环,易出竞态;而 PLINQ 自动管理线程间数据传递和异常聚合。
- PLINQ 默认启用
AsOrdered(),若不需要顺序,务必加AsUnordered()提升性能 - PLINQ 的
WithDegreeOfParallelism(n)比 Parallel 类的ParallelOptions.MaxDegreeOfParallelism更易控并发数 - PLINQ 在遇到第一个异常时默认停止(
AggregateException包含所有子异常);Parallel 类则需手动捕获并处理AggregateException.InnerExceptions
Parallel.Invoke 容易被忽略的执行边界
Parallel.Invoke 适合并行执行几个**彼此独立、无依赖、耗时相近**的任务。但它不返回值,也不提供进度反馈,常被误用于“启动一堆异步操作”——这其实是 Task.WhenAll 的职责。
典型误用:
Parallel.Invoke(() => File.WriteAllText("a.txt", dataA), () => File.WriteAllText("b.txt", dataB));
这里同步写文件会阻塞线程池线程,严重拖慢其他并行任务。
- IO 密集型操作(文件、网络、数据库)一律改用
async/await+Task.WhenAll,而非Parallel.Invoke -
Parallel.Invoke的每个 action 是同步执行的,超时、取消需自行在 action 内实现(如传入CancellationToken并检查) - 若任务耗时差异极大(一个 10ms,一个 5s),
Parallel.Invoke无法动态调整资源分配,不如手写Task.Run控制更细
实际项目里,并行逻辑一旦涉及共享状态、IO 或异常多样性,就很容易从“提升性能”滑向“难以调试”。Parallel 类表面简单,但线程安全、资源争用、取消机制这些点,几乎每个都得单独验证。











