You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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,把整个文件操作逻辑放在锁内,并且要先读取已有文件的内容再追加数据(避免覆盖):
    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");
            }
        }
    }
    
    注意:NPOI的多数操作没有异步版本,用lock同步比异步锁更简单可靠。

4. 分文件再合并的方案

如果数据量极大,并行写入单文件性能瓶颈明显,可以:

  • 每个线程写入单独的临时文件,比如result_{batchSysId}.xlsx
  • 所有线程完成后,用NPOI把所有临时文件的数据合并到最终的result.xlsx中
  • 最后删除临时文件

这种方案适合超大数据量场景,但实现复杂度比前两种高。


内容的提问来源于stack exchange,提问作者Nayeer Mahiuddin

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.06 19:35:25