parallel.for并非总能提速,单次迭代过短、存在共享写入或非cpu密集型任务时反而更慢;常见错误包括轻量计算并行化、非线程安全集合操作及未处理aggregateexception。

Parallel.For 不是“换掉 for 就变快”的魔法开关,它只在单次迭代 ≥ 几毫秒、无共享写入、CPU 密集型场景下才值得用。
Parallel.For 什么时候反而更慢?
常见错误现象:把一个纯数学计算的 for 循环(比如 i * i + i % 7)直接替换成 Parallel.For,结果耗时翻倍。
原因很简单:线程调度、任务分片、上下文切换的开销,远超单次迭代本身的执行时间。实测发现,若 Stopwatch 测得单次循环体平均
适用场景必须同时满足:
- 每次迭代逻辑独立,不读写同一块内存(如
list[i]是安全的,但sharedCounter++不安全) - 计算量足够大——典型如蒙特卡洛抽样、图像像素处理、矩阵元素变换
- 输入数据已加载到内存,不包含阻塞 I/O(如文件读取、HTTP 调用)
如何避免 IndexOutOfRangeException 和数据覆盖?
错误写法:Parallel.For(0, list.Count, i => list[i] = Process(list[i])); —— 看似和普通 for 一样,但 list 若是 List<t></t> 且非只读,多个线程可能同时写同一索引,或因扩容导致结构变化引发异常。
正确做法:
- 确保目标容器线程安全:用
T[]数组(索引写入天然安全)、ConcurrentBag<t></t>或ConcurrentQueue<t></t>收集结果 - 避免在循环体内修改集合长度或结构(如
Add、Remove) - 若必须更新共享变量,用
Interlocked.Add(ref sum, value)替代sum += value
示例(安全累加):
long sum = 0;
Parallel.For(0, 1000000, () => 0L, (i, state, localSum) => localSum + i * i,
finalSum => Interlocked.Add(ref sum, finalSum));
为什么默认并发度经常不是最优?
Parallel.For 默认使用线程池自动分配线程数,对 CPU 密集型任务来说,往往设为 Environment.ProcessorCount 最稳。但现实常有例外:
- 你的循环体里混了少量 I/O(如日志写入),线程数过多会加剧上下文切换,反而拖慢整体吞吐
- 程序跑在虚拟机或容器中,
Environment.ProcessorCount可能返回物理核数而非可用 vCPU 数 - 你同时运行多个
Parallel.For块,未设限会导致线程爆炸
务必显式控制:
var options = new ParallelOptions { MaxDegreeOfParallelism = 4 };
Parallel.For(0, data.Length, options, i => { /* ... */ });
调试时可打印 Environment.CurrentManagedThreadId 观察实际线程分布,别只信理论值。
AggregateException 是不是必须 catch?
是。哪怕你只预期一个异常,Parallel.For 也会把所有线程抛出的异常打包进 AggregateException。不捕获就直接崩。
典型错误:只写 catch (Exception e),漏掉内层异常;或只取 e.InnerException,忽略 e.InnerExceptions 可能含多个错误。
安全写法:
try
{
Parallel.For(0, n, i => { /* 可能抛异常的代码 */ });
}
catch (AggregateException ae)
{
foreach (var ex in ae.InnerExceptions)
{
Console.WriteLine($"Thread {ex.Source}: {ex.Message}");
}
}
真正容易被忽略的是:异常发生后,Parallel.For 默认立即中止其余所有正在运行的迭代。如果你需要“尽力而为”,得配合 CancellationToken 手动控制中断策略。











