C# Parallel.ForEach并行处理文件时偶发卡死问题排查
Parallel.ForEach偶发卡死问题排查
你的代码存在多个会导致卡死、执行异常的编写问题,核心问题如下:
- 线程安全问题(卡死核心原因):代码中使用的
areaDataCRCs、filesToMove、gr2Files均为非线程安全的普通集合(大概率是List<T>)。Parallel.ForEach会启动多个线程同时执行循环逻辑,多线程同时对普通集合执行Contains、Add、Remove操作时,会破坏集合内部的索引、数组结构,轻则出现数据丢失、异常,重则触发集合内部遍历的无限循环,表现为程序偶发卡死。 - 文件读取逻辑Bug:外层
while ((line = sr.ReadLine()) != null)已经读取了一行内容,进入循环后立刻又执行了一次line = sr.ReadLine(),会直接跳过所有奇数行内容;如果文件行数为奇数,最后一次读取会得到null,直接传入areaDataCRCs.Contains(null)会触发空引用异常,并行场景下未捕获的异常可能导致任务异常终止、状态错乱。 - 并行度设置无效:开头写的
Math.Max(2, Environment.ProcessorCount - 2);没有赋值给任何变量,完全不生效。另外读文件属于IO密集型操作,不是CPU密集型计算,默认的并行度会启动过多线程抢占磁盘IO资源,反而会降低处理速度。 - 后缀判断逻辑不严谨:使用
allPrFile.IndexOf(".prb") > -1判断文件类型,会把文件名包含.prb但后缀不是prb的文件(比如xxx.prb_bak)误判为目标文件。
修正后参考代码
// 设置合理的IO并行度,不要超过磁盘并发能力,一般4-8即可 int parallelCount = Math.Min(8, Math.Max(2, Environment.ProcessorCount - 2)); // 用线程安全集合替换普通List,ConcurrentDictionary做去重存储性能远高于ConcurrentBag ConcurrentDictionary<string, byte> areaDataCRCs = new ConcurrentDictionary<string, byte>(/* 初始化把你的CRC项传进去,值传0即可 */); ConcurrentDictionary<string, byte> filesToMove = new ConcurrentDictionary<string, byte>(); ConcurrentDictionary<string, byte> gr2Files = new ConcurrentDictionary<string, byte>(); Parallel.ForEach(allPrFiles, new ParallelOptions() { MaxDegreeOfParallelism = parallelCount }, allPrFile => { // 先判断文件后缀,避免打开不需要处理的文件浪费性能 if (Path.GetExtension(allPrFile)?.ToLower() != ".prb") { // 非prb文件只需要逐行判断是否包含CRC项即可 using (StreamReader sr = new StreamReader(allPrFile)) { string? line; while ((line = sr.ReadLine()) != null) { if (!string.IsNullOrEmpty(line) && areaDataCRCs.ContainsKey(line)) { Dbg.DebugTxt(allPrFile); areaDataCRCs.TryRemove(line, out _); break; } } } return; } // 处理prb文件 using (StreamReader sr = new StreamReader(allPrFile)) { string? line; bool foundCrc = false; while ((line = sr.ReadLine()) != null) { if (string.IsNullOrEmpty(line)) continue; if (areaDataCRCs.ContainsKey(line)) { Dbg.DebugTxt(allPrFile); areaDataCRCs.TryRemove(line, out _); foundCrc = true; filesToMove.TryAdd(allPrFile, 0); break; } } if (!foundCrc) return; // 继续读后续行找gr2文件 while ((line = sr.ReadLine()) != null) { if (string.IsNullOrEmpty(line)) continue; line = line.Replace("buildingfile", string.Empty); line = line.Replace("dungeonblockfile", string.Empty); line = line.Replace("\"", string.Empty); line = line.Trim(); if (string.IsNullOrEmpty(line)) { Dbg.DebugTxt("NO GR2 FILE FOUND - " + allPrFile); break; } gr2Files.TryAdd(line, 0); filesToMove.TryAdd(line, 0); #if DEBUG Dbg.DebugTxt(line); #endif break; } } }); // 如果最终需要List结果,执行完并行后再从ConcurrentDictionary转换即可 List<string> finalFilesToMove = filesToMove.Keys.ToList(); List<string> finalGr2Files = gr2Files.Keys.ToList();
注:代码中字符串裁剪部分按照你的说明保留了原有测试逻辑
内容的提问来源于stack exchange,提问作者x3Syntax
相关产品推荐
相关产品推荐

