Подтвердить что ты не робот

Исключить исключение из диапазона при использовании цикла 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: 
4b9b3361

Ответ 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) = >