使用ParallelFor循环时索引超出范围异常

Raz*_*t4x 14 c# multithreading .net-4.0 task-parallel-library

这是一个非常奇怪的情况,首先是代码......

代码

 private List<DispatchInvoiceCTNDataModel> WorksheetToDataTableForInvoiceCTN(ExcelWorksheet excelWorksheet, int month, int year)
        {
            int totalRows = excelWorksheet.Dimension.End.Row;
            int totalCols = excelWorksheet.Dimension.End.Column;
            DataTable dt = new DataTable(excelWorksheet.Name);
            // for (int i = 1; i <= totalRows; i++)
            Parallel.For(1, totalRows + 1, (i) =>
            {
                DataRow dr = null;
                if (i > 1)
                {
                    dr = dt.Rows.Add();
                }
                for (int j = 1; j <= totalCols; j++)
                {
                    if (i == 1)
                    {
                        var colName = excelWorksheet.Cells[i, j].Value.ToString().Replace(" ", String.Empty);
                        lock (lockObject)
                        {
                            if (!dt.Columns.Contains(colName))
                                dt.Columns.Add(colName);
                        }
                    }
                    else
                    {
                        dr[j - 1] = excelWorksheet.Cells[i, j].Value != null ? excelWorksheet.Cells[i, j].Value.ToString() : null;
                    }
                }
            });
            var excelDataModel = dt.ToList<DispatchInvoiceCTNDataModel>();
            // now we have mapped everything expect for the IDs
            excelDataModel = MapInvoiceCTNIDs(excelDataModel, month, year, excelWorksheet);
            return excelDataModel;
        }
Run Code Online (Sandbox Code Playgroud)

问题
当我在随机场合运行代码时,它会抛出IndexOutOfRangeException线

  dr[j - 1] = excelWorksheet.Cells[i, j].Value != null ? excelWorksheet.Cells[i, j].Value.ToString() : null;
Run Code Online (Sandbox Code Playgroud)

对于一些随机值ij.当我跳过代码(F10),因为它在ParallelLoop中运行,其他一些线程踢,而其他异常是抛出,另一个异常是这样的(我无法重现它,它只是来过一次,但我认为它也与此线程问题有关)Column 31 not found in excelWorksheet.我不明白这些异常怎么会发生?

案例1
IndexOutOfRangeException甚至不应该发生,作为唯一的代码/共享变量dt我身边有访问它锁定,其余全部是本地或参数,所以不应该有任何线程相关的问题.另外,如果我在调试窗口中检查i或调整j窗口的值,或者甚至在调试窗口中评估整个表达式 dr[j - 1] = excelWorksheet.Cells[i, j].Value != null ? excelWorksheet.Cells[i, j].Value.ToString() : null;或其中的一部分,那么它工作正常,没有任何类型的错误或什么也没有.

情况2
对于第二个错误,(遗憾的是现在不再复制,但仍然)它不应该发生,因为excel中有33列.

更多代码
如果有人可能需要如何调用此方法

using (var xlPackage = new ExcelPackage(viewModel.postedFile.InputStream))
            {
                ExcelWorksheets worksheets = xlPackage.Workbook.Worksheets;

                // other stuff 
                var entities = this.WorksheetToDataTableForInvoiceCTN(worksheets[1], viewModel.Month, viewModel.Year);
                // other stuff 
            }
Run Code Online (Sandbox Code Playgroud)

其他
如果有人需要更多代码/细节,请告诉我.

更新
好的,回答一些评论.它在使用工作正常for循环,我测试过很多次.此外,没有特定的值ij抛出异常的值.有时它8, 6在其他时间可能是任何东西,说19,2或任何东西.此外,在Parallel循环+1中没有造成任何损害,因为msdn文档说它是独占的,不包括在内.此外,如果那是问题,我只会在最后一个索引(i的最后一个值)获得异常,但事实并非如此.

更新2
锁定代码的给定答案

  dr = dt.Rows.Add();
Run Code Online (Sandbox Code Playgroud)

我把它改成了

  lock(lockObject) {
      dr = dt.Rows.Add();
  }
Run Code Online (Sandbox Code Playgroud)

它不起作用.现在我得到ArgumentOutOfRangeException,还是当我在调试窗口运行它,它只是正常工作.

更新3
以下是更新2之后的完整异常详细信息(我在更新2中提到的这一行上获得此内容)

System.ArgumentOutOfRangeException was unhandled by user code
  HResult=-2146233086
  Message=Index was out of range. Must be non-negative and less than the size of the collection.
Parameter name: index
  Source=mscorlib
  ParamName=index
  StackTrace:
       at System.ThrowHelper.ThrowArgumentOutOfRangeException()
       at System.Collections.Generic.List`1.get_Item(Int32 index)
       at System.Data.RecordManager.NewRecordBase()
       at System.Data.DataTable.NewRecordFromArray(Object[] value)
       at System.Data.DataRowCollection.Add(Object[] values)
       at AdminEntity.BAL.Service.ExcelImportServices.<>c__DisplayClass2e.<WorksheetToDataTableForInvoiceCTN>b__2d(Int32 i) in C:\Projects\Manager\Admin\AdminEntity\AdminEntity.BAL\Service\ExcelImportServices.cs:line 578
       at System.Threading.Tasks.Parallel.<>c__DisplayClassf`1.<ForWorker>b__c()
  InnerException: 
Run Code Online (Sandbox Code Playgroud)

Dio*_*nin 14

好的.因此,现有代码存在一些问题,其中大部分都被其他代码所触及:

  • 并行线程受操作系统调度程序的支配; 因此,尽管线程按顺序排队,但它们可能(并且经常会)无序地完成执行.例如,给定Parallel.For(0, 10, (i) => { Console.WriteLine(i); });,前四个线程(在四核系统上)将排队,i值为0-3.但是这些线程中的任何一个都可以在任何其他线程之前开始或完成.因此,您可能会先看到2个打印,然后第4个线程将排队.然后线程1可能完成,线程5将排队.然后线程4可能完成,甚至在线程0或3之前.等等TL; DR:您不能并行地假设有序输出.
  • 鉴于此,正如@ScottChamberlain所指出的那样,在并行循环中进行列生成是一个非常糟糕的主意 - 因为您无法保证执行列生成的线程将在另一个线程开始将行中的数据分配给这些列索引之前创建所有列.例如,您可以在表格实际具有第五列之前将数据分配给单元格[0,4].
    • 值得注意的是,无论如何,这应该从循环中完全打破,纯粹是从代码清洁度的角度来看.目前,您有两个嵌套循环,每个循环在一次迭代中具有特殊行为; 最好将该设置逻辑分离到自己的循环中,并让主循环分配数据而不是其他任何东西.
  • 出于同样的原因,您不应该在并行循环中的表中创建新行 - 因为您无法保证将按行的顺序将行添加到表中.打破这一点,并根据索引访问循环内的行.
  • 有人提到在Rows.Add()之前使用DataRow.NewRow(). 从技术上讲,NewRow()是处理事物的正确方法,但实际推荐的访问模式有点不同于逐个单元格的功能,特别是在有并行性时(参见MSDN:DataTable.NewRow方法)).事实上,使用Rows.Add()向DataTable添加一个新的空行并在之后填充它可以正常运行.
  • 您可以使用null-coalescing运算符清理字符串格式,该运算符??计算前面的值是否为null,如果是,则指定后续值.例如,foo = bar ?? ""相当于if (bar == null) { foo = ""; } else { foo = bar; }.

所以,你的代码看起来应该更像这样:

private void ReadIntoTable(ExcelWorksheet sheet)
{
    DataTable dt = new DataTable(sheet.Name);
    int height = sheet.Dimension.Rows;
    int width = sheet.Dimension.Columns;

    for (int j = 1; j <= width; j++)
    {
        string colText = (sheet.Cells[1, j].Value ?? "").ToString();
        dt.Columns.Add(colText);
    }
    for (int i = 2; i <= height; i++)
    {
        dt.Rows.Add();
    }

    Parallel.For(1, height, (i) =>
    {
        var row = dt.Rows[i - 1];
        for (int j = 0; j < width; j++)
        {
            string str = (sheet.Cells[i + 1, j + 1].Value ?? "").ToString();
            row[j] = str;
        }
    });

    // convert to your special Excel data model
    // ...
}
Run Code Online (Sandbox Code Playgroud)

好多了!

......但它仍然不起作用!

是的,它仍然因IndexOutOfRange异常而失败.但是,由于我们采用原始线dr[j - 1] = excelWorksheet.Cells[i, j].Value != null ? excelWorksheet.Cells[i, j].Value.ToString() : null;并将其拆分成几块,我们可以确切地看到它失败的部分.它失败了row[j] = str;,我们实际上将文本写入行.

嗯,哦.

MSDN:DataRow类

线程安全

此类型对于多线程读取操作是安全的.您必须同步任何写操作.

*叹*.是啊.谁知道为什么DataRow在分配值时会使用静态的东西,但是你有它; 写入DataRow不是线程安全的.当然,这样做......

private static object s_lockObject = "";

private void ReadIntoTable(ExcelWorksheet sheet)
{
    // ...
    lock (s_lockObject)
    {
        row[j] = str;
    }
    // ...
}
Run Code Online (Sandbox Code Playgroud)

......神奇地让它发挥作用.当然,它完全破坏了并行性,但它确实有效.

好吧,它几乎彻底摧毁了并行性.对包含18列和46319行的Excel文件进​​行的轶事实验表明,Parallel.For()循环平均在大约3.2s内创建其DataTable,而用for (int i = 1; i < height; i++)大约3.5s 替换Parallel.For().我的猜测是,由于锁仅用于写入数据,因此通过在一个线程上写入数据并在另一个线程上处理文本来实现非常小的好处.

当然,如果你可以创建自己的DataTable替换类,你可以看到更大的速度提升.例如:

string[,] rows = new string[height, width];
Parallel.For(1, height, (i) =>
{
    for (int j = 0; j < width; j++)
    {
        rows[i - 1, j] = (sheet.Cells[i + 1, j + 1].Value ?? "").ToString();
    }
});
Run Code Online (Sandbox Code Playgroud)

对于上面提到的同一个Excel表,这平均执行大约1.8秒 - 大约是我们几乎没有并行DataTable的一半时间.使用此片段中的()标准替换Parallel.For()使其在大约2.5秒内运行.

所以你可以从并行性看到显着的性能提升,但也可以从自定义数据结构中看到 - 虽然后者的可行性取决于你能否轻松地将返回值转换为Excel数据模型的东西,无论它是什么.

  • 球!这是很多*和*一个非常好的解释。让我试试看,我会发布更新。 (2认同)

Sco*_*ain 5

线 dr = dt.Rows.Add();不是线程安全的,您正在破坏保存表行的 DataTable 中数组的内部状态。

乍一看把它改成

if (i > 1)
{
    lock (lockObject)
    {
        dr = dt.Rows.Add();
    }
}
Run Code Online (Sandbox Code Playgroud)

应该修复它,但这并不意味着excelWorksheet.Cells从多个线程访问其他线程安全问题不存在。(如果excelWorksheet这个类并且您正在运行 STA 主线程(WinForms 或 WPF)COM 应该为您编组跨线程调用)


编辑:新理论,问题来自于您在并行循环内设置架构并尝试同时写入它的事实。将所有i == 1逻辑拉到循环之前,然后从i == 2

private List<DispatchInvoiceCTNDataModel> WorksheetToDataTableForInvoiceCTN(ExcelWorksheet excelWorksheet, int month, int year)
{
    int totalRows = excelWorksheet.Dimension.End.Row;
    int totalCols = excelWorksheet.Dimension.End.Column;
    DataTable dt = new DataTable(excelWorksheet.Name);

    //Build the schema before we loop in parallel.
    for (int j = 1; j <= totalCols; j++)
    {
        var colName = excelWorksheet.Cells[1, j].Value.ToString().Replace(" ", String.Empty);
        if (!dt.Columns.Contains(colName))
            dt.Columns.Add(colName);
    }

    Parallel.For(2, totalRows + 1, (i) =>
    {
        DataRow dr = null;
        lock(lockObject) {
            dr = dt.Rows.Add();
        }
        for (int j = 1; j <= totalCols; j++)
        {
            dr[j - 1] = excelWorksheet.Cells[i, j].Value != null ? excelWorksheet.Cells[i, j].Value.ToString() : null;
        }
    });
    var excelDataModel = dt.ToList<DispatchInvoiceCTNDataModel>();
    // now we have mapped everything expect for the IDs
    excelDataModel = MapInvoiceCTNIDs(excelDataModel, month, year, excelWorksheet);
    return excelDataModel;
}
Run Code Online (Sandbox Code Playgroud)