Исключить исключение из диапазона при использовании цикла ParallelFor
Это очень странная ситуация, сначала код...
Код
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
. Я не понимаю, как могло произойти какое-либо из этих исключений?
case 1
IndexOutOfRangeException
также не должен появляться, поскольку единственная переменная кода/общей переменной dt
, которую я заблокировал при доступе к ней, остальное все либо является локальным, либо параметром, поэтому не должно быть проблем, связанных с потоком. Кроме того, если я проверяю значение i
или j
в окне отладки или даже оцениваю это целое выражение dr[j - 1] = excelWorksheet.Cells[i, j].Value != null ? excelWorksheet.Cells[i, j].Value.ToString() : null;
или его часть в окне отладки, то он работает отлично, никаких ошибок или ничего.
case 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:
Ответы
Ответ 1
Хорошо. Таким образом, есть несколько проблем с вашим существующим кодом, большинство из которых были затронуты другими:
- Параллельные потоки находятся во власти планировщика ОС; поэтому, несмотря на то, что потоки поставлены в очередь в порядке, они могут (и часто делают) полное выполнение вне порядка. Например, с учетом
Parallel.For(0, 10, (i) => { Console.WriteLine(i); });
первые четыре потока (в четырехъядерной системе) будут помещены в очередь с i
значениями 0-3. Но любой из этих потоков может начаться или закончить выполнение перед любым другим. Таким образом, вы можете увидеть 2 напечатанных сначала, после чего поток 4 будет поставлен в очередь. Тогда поток 1 может завершиться, а поток 5 будет поставлен в очередь. Тогда поток 4 может завершиться, даже до того, как будут выполняться потоки 0 или 3. И т.д. И т.д. TL; DR: вы НЕ МОЖЕТЕ использовать параллельный параллельный вывод.
- Учитывая, что, как отметил @ScottChamberlain, очень сложно создать генерацию столбцов в вашем параллельном цикле, потому что у вас нет гарантии, что поток, выполняющий создание столбцов, создаст все ваши столбцы, прежде чем другой поток начнет назначать данные в строках эти индексы столбцов. Например. вы можете назначить данные ячейке [0,4], прежде чем ваша таблица будет иметь пятый столбец.
- Стоит отметить, что это действительно должно быть выведено из цикла в любом случае, чисто с точки зрения чистоты кода. На данный момент у вас есть две вложенные петли, каждая из которых имеет особое поведение на одной итерации; лучше отделить эту логику установки в свой собственный цикл и оставить основной цикл для назначения данных и ничего другого.
- По той же причине вы не должны создавать новые строки в таблице в своем параллельном цикле, потому что у вас нет гарантии, что строки будут добавлены в таблицу в исходном порядке. Разбейте это тоже и получите доступ к строкам в цикле на основе их индекса.
- Некоторые упомянули использование DataRow.NewRow() до Rows.Add(). Технически, NewRow() - это правильный способ обойти все, но фактический рекомендуемый шаблон доступа немного отличается от того, который, вероятно, подходит для функции "по ячейкам", особенно когда предназначен parallelism (см. MSDN: метод DataTable.NewRow). Факт остается фактом: добавление новой пустой строки в DataTable с помощью Rows.Add() и ее последующее функционирование корректно функционирует.
- Вы можете очистить форматирование строки с помощью оператора с нулевым коалесцированием
??
, который оценивает, является ли предыдущее значение нулевым, и если да, то присваивает последующее значение. Например, 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
Безопасность потоков
Этот тип безопасен для многопоточных операций чтения. Вы должны синхронизировать любые операции записи.
* Вздох *. Да. Кто знает, почему DataRow использует статические объекты при назначении значений, но там у вас это есть; запись в DataRow не является потокобезопасной. И конечно же, делая это...
private static object s_lockObject = "";
private void ReadIntoTable(ExcelWorksheet sheet)
{
// ...
lock (s_lockObject)
{
row[j] = str;
}
// ...
}
... магически заставляет его работать. Конечно, он полностью разрушает parallelism, но он работает.
Ну, он почти полностью уничтожает parallelism. Анекдотические эксперименты над файлом Excel с 18 столбцами и 46319 строк показывают, что цикл Parallel.For() создает свой DataTable примерно в 3,2 раза, тогда как замена Parallel.For() на for (int i = 1; i < height; i++)
занимает около 3,5 с. Я предполагаю, что поскольку блокировка существует только для записи данных, очень небольшое преимущество достигается за счет записи данных в один поток и обработки текста на других.
Конечно, если вы можете создать свой собственный класс замены 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();
}
});
Это выполняется примерно в 1,8 раза для той же таблицы Excel, о которой говорилось выше - примерно в половине случаев нашей едва параллельной DataTable. Замена Parallel.For() со стандартом для() в этом фрагменте делает его запуском примерно в 2,5 с.
Таким образом, вы можете увидеть значительное повышение производительности от parallelism, но также и от пользовательской структуры данных, хотя жизнеспособность последнего будет зависеть от вашей способности легко преобразовывать возвращаемые значения в эту модель данных Excel, независимо от того, есть.
Ответ 2
Строка dr = dt.Rows.Add();
не является потокобезопасной, вы повредите внутреннее состояние массива в DataTable, в котором хранятся строки для таблицы.
На первый взгляд, изменив его на
if (i > 1)
{
lock (lockObject)
{
dr = dt.Rows.Add();
}
}
должен исправить это, но это не означает, что другие проблемы безопасности потока не существуют из excelWorksheet.Cells
, которые доступны из нескольких потоков. (Если excelWorksheet
этот класс, и вы используете основной поток STA (WinForms или WPF), COM должен маршировать вызовы перекрестных потоков для вас)
EDIT: Новая проблема, проблема возникает из-за того, что вы настраиваете свою схему внутри параллельного цикла и пытаетесь записать ее одновременно. Вытяните всю логику 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;
}
Ответ 3
Код неправильный:
1) Parallel.For имеет свой собственный механизм пакетной обработки (может быть настроен с помощью ForEach с разделителями) и не гарантирует, что операция с (for) я == n будет выполняться после операции с я == m, где n > м.
Итак, строка
dr[j - 1] = excelWorksheet.Cells[i, j].Value != null ? excelWorksheet.Cells[i, j].Value.ToString() : null;
throw exception, когда требуемый столбец еще не добавлен (в операции {i == 1}}
2) И рекомендуется использовать метод NewRow:
dr=tbl.NewRow->Populate dr->tbl.Rows.Add(dr)
или Rows.Add(значения объекта []):
values=[KnownColumnCount]->Populate values->tbl.Rows.Add(values)
3) В этом случае лучше всего заполнить столбцы, потому что это последовательный доступ к файлу excel (поиск), и это не повредит производительности
Ответ 4
Вы пытались использовать NewRow при создании нового datarow и перемещать создание столбцов вне параллельного цикла, как Скотт Чемберлен, предложенный выше? Используя 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);
}
}
});
Ответ 5
ты пробовал
Параллельно. Для (0, totalRows, (i) = >