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

时间:2015-04-09 14:47:01

标签: 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;
        }

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

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

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

案例1
IndexOutOfRangeException甚至不应该出现,因为唯一的代码/共享变量dt我已经锁定了访问它,休息全部是本地或参数所以不应该有任何线程相关的问题。另外,如果我在调试窗口中检查ij的值,或者甚至在调试窗口中评估整个表达式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 
            }

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

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

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

  dr = dt.Rows.Add();

我已将其更改为

  lock(lockObject) {
      dr = dt.Rows.Add();
  }

它不起作用。现在我得到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: 

4 个答案:

答案 0 :(得分:14)

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

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

好多了!

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

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

糟糕,

MSDN: DataRow Class

  

线程安全

     

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

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

private static object s_lockObject = "";

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

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

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

当然,如果你可以创建自己的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();
    }
});

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

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

答案 1 :(得分:4)

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

乍一看将其改为

if (i > 1)
{
    lock (lockObject)
    {
        dr = dt.Rows.Add();
    }
}

应该修复它,但这并不意味着从多个线程访问excelWorksheet.Cells不存在其他线程安全问题。 (如果excelWorksheetthis class并且您正在运行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;
}

答案 2 :(得分:3)

您的代码不正确:

1)Parallel.For有自己的批处理机制(虽然可以使用分区程序使用ForEach进行自定义)并且不保证在使用i == m(其中n&gt;)操作后将执行带有(for)i == n的操作。米 所以行

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

在尚未添加所需列时抛出异常(在{i == 1} operation}

2)建议使用NewRow方法:

dr=tbl.NewRow->Populate dr->tbl.Rows.Add(dr)

或Rows.Add(object [] values):

values=[KnownColumnCount]->Populate values->tbl.Rows.Add(values)

3)在这种情况下首先填充列真的更好,因为它可以顺序访问excel文件(搜索)并且不会损害性能

答案 3 :(得分:0)

您是否尝试过在创建新数据流时使用NewRow并在上面提到的Scott Chamberlain建议的并行循环之外移动列的创建?通过使用newrow,您将创建一个与父数据表具有相同模式的行。当我使用随机excel文件尝试你的代码时,我得到了与你相同的错误,但让它像这样工作:

            for (int x = 1; x <= totalCols; x++)
        {
            var colName = excelWorksheet.Cells[1, x].Value.ToString().Replace(" ", String.Empty);

            if (!dt.Columns.Contains(colName))
                dt.Columns.Add(colName);

        }

        Parallel.For(2, totalRows + 1, (i) =>
        {
            DataRow dr = null;

            for (int j = 1; j <= totalCols; j++)
            {
                dr = dt.NewRow();
                dr[j - 1] = excelWorksheet.Cells[i, j].Value != null
                    ? excelWorksheet.Cells[i, j].Value.ToString()
                    : null;
                lock (lockObject)
                {
                    dt.Rows.Add(dr);
                }
            }
        });