Parallel.ForEach 不是 foreach 的“加速开关”,用错会丢数据、抛异常或更慢;需用线程安全集合、避免共享状态、按场景选 Parallel.ForEach 或 ForEachAsync,并注意数据规模与并行度。

Parallel.ForEach 不是 foreach 的“加速开关”,用错会丢数据、抛 Collection was modified 异常,甚至比单线程还慢。
为什么一跑就报“集合已修改”异常
最常见原因是多个线程同时往非线程安全集合(比如 List<t></t>、Dictionary<k></k>)里调用 Add 或 AddRange。.NET 普通集合类不保证并发写入安全,而 Parallel.ForEach 默认就是多线程并行执行。
- 别在循环体里直接写
results.Add(item)—— 改用ConcurrentBag<t></t>、ConcurrentQueue<t></t>或加lock - 如果只是累加数字,优先用带局部状态的重载(
localInit/localFinally),避免所有线程争抢同一个变量 - 调试时加
Thread.Sleep(1)反而更容易复现问题——这不是巧合,是暴露了竞态条件
Parallel.ForEach 和 ForEachAsync 到底该选哪个
看操作类型:CPU 密集型用 Parallel.ForEach,I/O 密集型必须用 Task.WhenAll + ForEachAsync(或手动 async/await)。
-
Parallel.ForEach的MaxDegreeOfParallelism控制的是线程数,不是并发请求数;对 HTTP 请求、文件读写这类 I/O 操作,它只会开一堆阻塞线程,浪费资源 -
ForEachAsync控制的是异步任务并发度,更贴合 I/O 场景,且不会占用线程池线程 - 混合场景(比如先 CPU 计算再发 HTTP)要拆开:用
Parallel.ForEach做计算,结果再喂给Task.WhenAll
如何避免 Parallel.ForEach 跑得比 foreach 还慢
并行有开销:线程调度、数据分区、结果聚合。小数据量或简单逻辑下,并行反而拖后腿。
- 实测阈值因机器而异,但一般
source.Count < 1000且每项耗时 < 1ms 时,foreach更稳更快 -
MaxDegreeOfParallelism设成Environment.ProcessorCount是合理起点;设成 100 不会变快,只会增加上下文切换 - 源集合要是支持高效随机访问的(
IList<t></t>或数组),否则Parallel.ForEach内部会先转成数组——隐式内存分配+拷贝
一个真正能用的安全模板
以下代码处理一批字符串,清洗后存入线程安全集合,同时避开常见陷阱:
var source = File.ReadAllLines("data.txt");
var results = new ConcurrentBag<string>();
Parallel.ForEach(source, new ParallelOptions { MaxDegreeOfParallelism = 4 }, line =>
{
var cleaned = line.Trim().ToUpperInvariant();
if (!string.IsNullOrEmpty(cleaned))
results.Add(cleaned); // ConcurrentBag.Add 是线程安全的
});
// 注意:ConcurrentBag 遍历时顺序不保证;如需有序,请转 List 后 Sort 或改用 ConcurrentDictionary + 索引
最容易被忽略的点是:即使用了线程安全集合,如果你在循环体里还去读写 UI 控件、静态字段、或依赖全局状态(比如某个静态 DateTime.Now 时间戳做判断),依然可能出错——并行的本质是打破执行顺序和上下文隔离,这点比锁更难调试。



















