Parallel.ForEachAsync结合NPOI写入同一Excel文件遇进程占用问题求助
问题描述
我写了如下Parallel.ForEachAsync循环:
await Parallel.ForEachAsync(batchSysIds, parallelOptions, async (batchSysId, _) => { await GenerateListWriteFile(batchSysId); });
对应的生成列表并写入文件的代码:
private static async Task GenerateListWriteFile(long batchSysId) { var newList= await GenerateList(batchSysId); await WriteToFile(newList); }
写入Excel文件的实现:
public async Task WriteToExcel(List<ResultFile> results) { DataTable table = (DataTable)JsonConvert.DeserializeObject(JsonConvert.SerializeObject(results), (typeof(DataTable))); var memoryStream = new MemoryStream(); try { using (var fs = new FileStream(File_Name, FileMode.OpenOrCreate, FileAccess.Write)) { IWorkbook workbook = new XSSFWorkbook(); ISheet excelSheet = workbook.CreateSheet("Sheet1"); List<string> columns = new(); IRow row = excelSheet.CreateRow(0); int columnIndex = 0; foreach (DataColumn column in table.Columns) { columns.Add(column.ColumnName); row.CreateCell(columnIndex).SetCellValue(column.ColumnName); columnIndex++; } int rowIndex = 1; foreach (DataRow dsrow in table.Rows) { row = excelSheet.CreateRow(rowIndex); int cellIndex = 0; foreach (String col in columns) { row.CreateCell(cellIndex).SetCellValue(dsrow[col].ToString()); cellIndex++; } rowIndex++; } workbook.Write(fs, false); workbook.Close(); } } catch (Exception ex) { logger.Error(ex, $"Error writing to file"); } }
运行时出现错误:
The process cannot access the file 'D:\result.xlsx' because it is being used by another process
我想知道:是否可以直接并行写入同一文件?是否需要为每个线程创建单独文件再合并?或是有更好的单文件写入方案?
编辑说明:我尝试用lock但没解决问题,修改后的代码如下:
public async Task WriteToExcel(List<ResultFile> results) { DataTable table = (DataTable)JsonConvert.DeserializeObject(JsonConvert.SerializeObject(results), (typeof(DataTable))); var memoryStream = new MemoryStream(); try { using (var fs = new FileStream(File_Name, FileMode.OpenOrCreate, FileAccess.Write)) { IWorkbook workbook = new XSSFWorkbook(); ISheet excelSheet = workbook.CreateSheet("Sheet1"); lock (workbook) { List<string> columns = new(); IRow row = excelSheet.CreateRow(0); int columnIndex = 0; foreach (DataColumn column in table.Columns) { columns.Add(column.ColumnName); row.CreateCell(columnIndex).SetCellValue(column.ColumnName); columnIndex++; } int rowIndex = 1; foreach (DataRow dsrow in table.Rows) { row = excelSheet.CreateRow(rowIndex); int cellIndex = 0; foreach (String col in columns) { row.CreateCell(cellIndex).SetCellValue(dsrow[col].ToString()); cellIndex++; } rowIndex++; } workbook.Write(fs, false); workbook.Close(); } } } catch (Exception ex) { logger.Error(ex, $"Error writing to file"); } }
解决方案
1. 为什么你的lock没生效?
你当前的lock对象是每个线程自己创建的workbook实例,不同线程的锁对象完全独立,根本起不到互斥作用。锁必须使用所有线程都能访问到的同一个静态对象才能实现同步。
2. 直接并行写入同一文件的可行性
可以实现,但必须保证写入操作完全串行化,而且你的现有代码还有另一个问题:每次写入都会创建新的XSSFWorkbook并覆盖文件,就算同步了,最后也只会保留最后一个线程写入的数据,之前的都会被覆盖。
3. 正确的单文件写入方案
方案A:并行生成数据,串行写入文件
把数据生成和文件写入拆分,先并行生成所有数据,再统一写入Excel,这是最简洁高效的方式:
// 并行生成所有数据 var allResults = await Task.WhenAll(batchSysIds.Select(async id => await GenerateList(id))); // 合并所有子列表的数据 var mergedResults = allResults.SelectMany(list => list).ToList(); // 串行写入文件 await WriteToExcel(mergedResults);
同时修改WriteToExcel为正常的单线程写入逻辑(不需要锁),确保一次性写入完整数据。
方案B:同步写入操作(保留并行生成)
如果必须在生成数据后立即写入(比如数据量太大,不想全部放在内存),需要:
- 定义一个全局静态锁对象:
private static readonly object _fileLock = new object(); - 修改
WriteToExcel,把整个文件操作逻辑放在锁内,并且要先读取已有文件的内容再追加数据(避免覆盖):
注意:NPOI的多数操作没有异步版本,用public async Task WriteToExcel(List<ResultFile> results) { DataTable table = (DataTable)JsonConvert.DeserializeObject(JsonConvert.SerializeObject(results), typeof(DataTable)); lock (_fileLock) { try { IWorkbook workbook; using (var fs = new FileStream(File_Name, FileMode.OpenOrCreate, FileAccess.ReadWrite)) { // 读取已有工作簿或创建新工作簿 if (fs.Length > 0) { workbook = new XSSFWorkbook(fs); fs.Seek(0, SeekOrigin.Begin); // 回到文件开头准备写入 } else { workbook = new XSSFWorkbook(); } ISheet excelSheet = workbook.GetSheet("Sheet1") ?? workbook.CreateSheet("Sheet1"); // 处理表头:仅当工作表为空时创建 int startRowIndex = excelSheet.LastRowNum + 1; if (startRowIndex == 0) { var columns = table.Columns.Cast<DataColumn>().Select(c => c.ColumnName).ToList(); IRow headerRow = excelSheet.CreateRow(0); for (int i = 0; i < columns.Count; i++) { headerRow.CreateCell(i).SetCellValue(columns[i]); } startRowIndex = 1; } // 写入数据行 foreach (DataRow dsrow in table.Rows) { IRow row = excelSheet.CreateRow(startRowIndex++); for (int i = 0; i < table.Columns.Count; i++) { row.CreateCell(i).SetCellValue(dsrow[i].ToString()); } } // 清空原有文件内容并写入新数据 fs.SetLength(0); workbook.Write(fs, false); workbook.Close(); } } catch (Exception ex) { logger.Error(ex, $"Error writing to file"); } } }lock同步比异步锁更简单可靠。
4. 分文件再合并的方案
如果数据量极大,并行写入单文件性能瓶颈明显,可以:
- 每个线程写入单独的临时文件,比如
result_{batchSysId}.xlsx - 所有线程完成后,用NPOI把所有临时文件的数据合并到最终的
result.xlsx中 - 最后删除临时文件
这种方案适合超大数据量场景,但实现复杂度比前两种高。
内容的提问来源于stack exchange,提问作者Nayeer Mahiuddin
相关产品推荐
相关产品推荐

