C# Parallel.ForEach循环漏执行、重复执行问题排查
并行代码迭代遗漏、重复执行问题排查
我有两套可实现相同功能的代码逻辑,一套为串行执行版本,另一套为并行执行版本。由于总迭代规模约为10万次,量级较大,因此采用并行化方案优化执行效率。但并行版本运行时出现异常:部分迭代被跳过未执行,部分迭代被重复执行,暂未找到有效的调试排查思路。
两套实现除了将普通foreach循环替换为Parallel.ForEach循环外,其余逻辑完全一致。
串行方法实现代码
for(int i = somenum; i >= 0; i-- ){ foreach (var nue in nuelist) { foreach (var path in nue.pathlist) { foreach (var conn in nue.connlist) { Func(conn,path); } } } }
并行方法实现代码
for(int i = somenum; i >= 0; i-- ){ Parallel.ForEach(nuelist,nue => { Parallel.ForEach(nue.pathlist,path=> { Parallel.ForEach(nue.connlist, conn=> { Func(conn,path); }); }); }); }
Path类内部实现代码
Nue firstnue; public void Func(Conn conn,Path path) { List<Conn> list = new(){conn}; list.AddRange(path.list); _ = new Path(list); } public Path(List<Conn>) { //other things firstnue.pathlist.Add(this); /* firstnue 属于外层for循环下一轮迭代会处理的nue对象 */ }
问题根因
并行版本执行结果不符合预期的核心原因是线程安全冲突,具体分为两点:
- 遍历非线程安全集合时并发修改集合:
Path构造函数中会向firstnue.pathlist新增元素,firstnue属于后续外层i循环的遍历对象。串行执行时新增的元素只会在后续轮次被遍历,不会干扰当前轮次逻辑;但并行执行时多线程同时对普通List<T>做读写操作——一边遍历pathlist,一边往里面执行Add操作,会直接导致List<T>内部索引结构错乱,既可能让新增元素提前被当前轮次遍历到(造成重复执行),也可能让原有元素被跳过,甚至触发“集合已修改,无法执行枚举操作”的异常。三层嵌套Parallel.ForEach会过度占用线程池资源,进一步放大这类冲突的出现概率。 - 嵌套并行打乱原有执行时序:
Parallel.ForEach是阻塞式执行,多层嵌套会让线程调度逻辑失控,无法保证串行版本中“当前外层i轮次所有逻辑执行完成后,再进入下一轮i循环”的执行顺序,进一步加剧执行结果和预期不符的问题。
修复方案
- 每轮外层
i循环启动时,先对当前需要遍历的所有集合做只读快照,遍历快照而非原始集合,从根源上避免遍历过程中集合变动的干扰:
for(int i = somenum; i >= 0; i-- ){ // 生成当前轮次待处理数据的快照,遍历过程不直接操作原集合 var currentNues = nuelist.ToArray(); Parallel.ForEach(currentNues, nue => { var currentPaths = nue.pathlist.ToArray(); var currentConns = nue.connlist.ToArray(); // 内层用普通foreach即可,不需要嵌套并行 foreach (var path in currentPaths) { foreach (var conn in currentConns) { Func(conn, path); } } }); }
- 移除不必要的嵌套
Parallel.ForEach,仅保留最外层一层并行即可。内层循环用普通foreach执行,既减少线程调度的额外开销,也能保证执行时序和串行版本一致。 - 如果存在多线程同时写入同一个集合的场景,要么对写入逻辑加锁保护,要么替换为
ConcurrentBag<T>这类线程安全集合,避免并发写冲突。
内容的提问来源于stack exchange,提问作者Atomic
相关产品推荐
相关产品推荐

