Parallel.For 和 Parallel.ForEach 默认将异常封装为 AggregateException 并中止执行;需在循环体内 try-catch 处理局部异常,用 Interlocked 或并发集合保障线程安全,小数据量时串行更优,I/O 密集型任务应限制 MaxDegreeOfParallelism。

Parallel.For 和 Parallel.ForEach 默认不捕获异常,一旦某个任务抛出未处理异常,整个并行操作会立即中止,并包装为 AggregateException —— 这是多数人踩坑的第一步。
Parallel.ForEach 遇到异常就崩?必须用 try-catch 包裹每个迭代体
默认情况下,Parallel.ForEach 不会像普通 foreach 那样让异常“冒泡”到调用线程;它把所有子任务异常收集起来,统一在最后抛出一个 AggregateException。但如果你希望单个元素失败不影响其余处理(比如日志解析、文件批量读取),就得把逻辑下沉到循环体内:
- 在
body委托内部加try/catch,自行记录错误或跳过 - 不要依赖外层
try/catch来“挽救”部分失败 —— 它只能拿到汇总异常,无法知道哪个索引/项出错 - 若需区分异常类型(如
IOException可重试,FormatException直接丢弃),必须在循环体内判断
Parallel.ForEach(files, file => {
try {
ProcessFile(file); // 可能抛出 IOException 或 NullReferenceException
}
catch (IOException ex) {
Log.Warn($"跳过文件 {file}:{ex.Message}");
}
catch (Exception ex) {
Log.Error($"处理 {file} 时未预期异常", ex);
}
});
Parallel.For 的索引不是线程安全的,别直接++共享变量
Parallel.For 的迭代变量(如 i)是每个线程独立副本,但如果你在循环体里修改外部变量(比如计数器 totalCount++),会因竞态导致结果错误。这不是“并行不支持”,而是没用对同步机制:
- 避免直接对
int、long等值类型做++操作 - 改用
Interlocked.Increment(ref count),它生成原子 add 指令,开销低且可靠 - 若需累积复杂对象(如列表、字典),优先考虑
ConcurrentBag或ConcurrentDictionary,而非加锁的List
例如统计成功处理的文件数:int successCount = 0;Parallel.ForEach(files, file => { if (TryProcess(file)) Interlocked.Increment(ref successCount); });
Parallel 并不总是比 for 快,小数据量 + 简单逻辑反而更慢
Parallel 类底层依赖 ThreadPool 分配任务,启动/调度/合并有固定开销。当单次迭代耗时低于 ~10μs,或者集合长度小于几百,串行 for 往往更快:
- 测试前先用
Stopwatch对比:纯计算型循环(如数组求和)在 10k 元素以下,Parallel.For常慢 20%~50% - I/O 密集型(如 HTTP 请求、文件读写)更适合并行,但应控制并发度,避免打爆连接池 —— 用
ParallelOptions.MaxDegreeOfParallelism限制线程数 - 注意 CPU 核心数 ≠ 最佳并行度;数据库查询类操作常设为
Environment.ProcessorCount / 2更稳
示例限流:var options = new ParallelOptions { MaxDegreeOfParallelism = 4 };Parallel.ForEach(urls, options, url => MakeHttpRequest(url));
真正难的不是写 Parallel.ForEach,而是判断「该不该并行」以及「怎么收尾」—— 比如并行写入同一个文件要加锁,而并行生成临时文件再合并,就完全不用操心线程安全。这些决策点不会出现在语法示例里,但决定了代码上线后能不能扛住真实流量。











