这是一个非常奇怪的情况,首先是代码......
代码
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;
对于i
和j
的某些随机值。当我跨过代码(F10
),因为它在ParallelLoop中运行,其他一些线程踢,而其他异常是throw,另一个异常是这样的(我无法重现它,它只是来过一次,但我认为这也与此线程问题有关)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
}
其他
如果有人需要更多代码/细节,请告诉我。
更新
好的,回答一些评论。使用for
循环时, 正常工作,我已多次测试过。此外,抛出异常的i
或j
没有特定值。有时它是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:
答案 0 :(得分:14)
好。因此,您现有的代码存在一些问题,其中大部分都被其他代码所触及:
Parallel.For(0, 10, (i) => { Console.WriteLine(i); });
,前四个线程(在四核系统上)将排队{0}值为i
。但是这些线程中的任何一个都可以在任何其他线程之前开始或完成。因此,您可能会先看到2个打印,然后第4个线程将排队。然后线程1可能完成,线程5将排队。然后线程4可能完成,甚至在线程0或3之前。等等; TL; DR:您不能并行地假设有序输出。??
清除字符串格式,该运算符计算前面的值是否为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;
失败,我们实际上将文本写入行。
糟糕,
线程安全
此类型对于多线程读取操作是安全的。您必须同步任何写操作。
*叹*。是啊。谁知道为什么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
不存在其他线程安全问题。 (如果excelWorksheet
是this 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);
}
}
});