Parallel.ForEach ничего или очень мало происходит

Я пытаюсь прочитать документ excel и записать его как csv.

Я понял, как это сделать несколькими немного разными способами, но это чертовски медленно.

это то, что у меня есть, работает и проходит через 2 арка с 16384 строками и 5 столбцами данных примерно за 1 минуту 36 секунд.

  public void ToCSV(Stream excelStream, int i)
    {
        // IExcelDataReader excelReader = null;

        using (var excelReader = ExcelReaderFactory.CreateReader(excelStream))
        {
            System.Data.DataSet excelsheets = excelReader.AsDataSet();

            foreach (DataTable aSheet in excelsheets.Tables)
            {
                string strCSVData = "";
                string sheetName = aSheet.TableName;

                foreach (DataRow row in aSheet.Rows)
                {
                    foreach (var column in row.ItemArray)
                    {
                        strCSVData += column.ToString().Replace(",", ",") + ",";
                    }
                    strCSVData += "\n";
                }
                string strOutputFileName = Directory.GetCurrentDirectory() + sheetName + i.ToString() + ".csv";
                StreamWriter csvFile = new StreamWriter(strOutputFileName, false);
                csvFile.Write(strCSVData);
                csvFile.Close();
            }
        }
    }

Сейчас пытаюсь ускорить. Я был немного быстрее, используя обычный цикл for, но ничего впечатляющего — около 1 минуты 33 секунды.

Поэтому я подумал, как насчет использования Parallel.foreach вместо этого. однако это привело либо к записи только трети данных, либо к тому, что в настоящее время их нет.

вот как я изменил вышеуказанный метод.

 public void ToCSVParallel(Stream excelStream, int i)
    {
        // IExcelDataReader excelReader = null;

        using (var excelReader = ExcelReaderFactory.CreateReader(excelStream))
        {
            System.Data.DataSet excelsheets = excelReader.AsDataSet();

            for (int sheet = 0; sheet < excelsheets.Tables.Count; sheet++)
            {
                DataTable aSheet = excelsheets.Tables[sheet];
                List<string> strCSVData = new List<string>();
                string sheetName = aSheet.TableName;
                IEnumerable<DataRow> dataSheet = aSheet.AsEnumerable();
                Parallel.ForEach<DataRow>(dataSheet, row =>
                {
                    string strRow = "";
                    for (int column = 0; column < row.ItemArray.Count(); column++)
                    {
                        strRow = row[column].ToString().Replace(",", "&comma;") + ",";
                    }
                    strRow += "\n";
                    strCSVData.Append(strRow);
                });

                string strOutputFileName = Directory.GetCurrentDirectory() + sheetName + i.ToString() + ".csv";
                //StreamWriter csvFile = new StreamWriter(strOutputFileName, false);
                System.IO.File.WriteAllLines(strOutputFileName, strCSVData);
              //  csvFile.Write(strCSVData);
                //csvFile.Close();
            }
        }
    }

Теперь я понятия не имею, что я делаю неправильно. Но я вполне уверен, что неправильно понимаю, как я могу использовать parallel.foreach, но что я делаю неправильно?

или есть лучший/умный/простой способ ускорить мой метод?

ИЗМЕНИТЬ:

Основываясь на всех ваших предложениях, я пришел к следующим изменениям.

public void ToCSVParallel(Stream excelStream, int i)
    {
        using (var excelReader = ExcelReaderFactory.CreateReader(excelStream))
        {
            System.Data.DataSet excelsheets = excelReader.AsDataSet();

            for (int sheet = 0; sheet < excelsheets.Tables.Count; sheet++)
            {
                DataTable aSheet = excelsheets.Tables[sheet];
                ConcurrentBag<string> strCSVData = new ConcurrentBag<string>();
                string sheetName = aSheet.TableName;
                IEnumerable<DataRow> dataSheet = aSheet.AsEnumerable();
                Parallel.ForEach<DataRow>(dataSheet, row =>
                {
                    StringBuilder strRow = new StringBuilder();
                    for (int column = 0; column < row.ItemArray.Count(); column++)
                    {
                        strRow.Append(row[column].ToString().Replace(",", "&comma;") + ",");
                    }
                    strCSVData.Add(strRow.ToString());
                });

                string strOutputFileName = Directory.GetCurrentDirectory() + sheetName + i.ToString() + ".csv";
                System.IO.File.WriteAllLines(strOutputFileName, strCSVData);
            }
        }
    }

Однако, основываясь на предложении @Magnus, я также изменил свой первоначальный метод на это:

public void ToCSV(Stream excelStream, int i)
    {
        using (var excelReader = ExcelReaderFactory.CreateReader(excelStream))
        {
            System.Data.DataSet excelsheets = excelReader.AsDataSet();
            foreach (DataTable aSheet in excelsheets.Tables)
            {
                string sheetName = aSheet.TableName;
                string strOutputFileName = Directory.GetCurrentDirectory() + sheetName + i.ToString() + ".csv";

                using (StreamWriter csvFile = new StreamWriter(strOutputFileName, false))
                {
                    foreach (DataRow row in aSheet.Rows)
                    {
                        foreach (var column in row.ItemArray)
                        {
                            csvFile.Write(column.ToString().Replace(",", "&comma;") + ",");
                        }
                        csvFile.WriteLine();
                    }
                }
            }
        }
    }

и результат меня поразил.

параллель в среднем на 1000 миллисекунд медленнее, чем модифицированные циклы Foreach.

однако моя идея сделать метод более быстрым теперь находится в пределах приемлемого уровня. параллель занимает в среднем около 8800 миллисекунд. цикл foreach занимает в среднем 7600 миллисекунд. оба из них находятся на 2 ковчегах с 16384 строками и 5 столбцами данных в каждом.


person Helbo    schedule 09.07.2018    source источник
comment
Глядя на это, я нервничаю по поводу strCSVData.Append в цикле. Я не думаю, что это потокобезопасная структура, и не думаю, что имеет смысл использовать ее здесь. Может быть, вместо этого вернуть значения, которые вы добавили, а затем объединить ParallelLoopResults? Хотя я предполагаю, что вы действительно хотите, чтобы это был один список на поток, добавьте в этот список, а затем просто объедините их.   -  person Rup    schedule 09.07.2018
comment
Не было бы намного проще использовать функциональные возможности Excel для экспорта в формате CSV?   -  person Camilo Terevinto    schedule 09.07.2018
comment
Поскольку вы используете асинхронную операцию, возможно, что многие записи происходят одновременно, и во время процесса записи лист заблокирован и отсутствуют некоторые вызовы записи. Лучше всего было бы запросить с помощью драйвера Excel и вывести все строки используя этот запрос.   -  person rmehra76    schedule 09.07.2018
comment
Обратите внимание, что Parallel.ForEach может запускать задачи в произвольном порядке. Также обратите внимание, что порядок завершения задач также произволен (это не обязательно в том порядке, в котором задачи были запущены). Ваш код теперь добавляет данные в strCSVData в том порядке, в котором задачи завершаются. Но вам лучше добавить данные strRow в strCSVData в порядке соответствующих строк данных Excel. Кроме того, может случиться так, что две задачи одновременно добавляются к strCSVData, существенно повреждая добавленные данные...   -  person    schedule 09.07.2018
comment
Для ясности, сколько времени потребуется, чтобы прочитать документ Excel, не пытаясь запустить Replace или записать его в CSV? Я думаю, что оптимизации можно разделить, прежде чем мы приступим к решению проблемы.   -  person Svek    schedule 09.07.2018
comment
В вашем обычном цикле пишите напрямую в StreamWriter вместо конкатенации в strCSVData.   -  person Magnus    schedule 09.07.2018
comment
Если вы скопировали/вставили свой код, там есть ошибка: strRow = row[column].ToString().Replace(",", "&comma;") + ",";. Вы имели в виду strRow += ... ?   -  person Cid    schedule 09.07.2018
comment
Вы не должны использовать список в нескольких потоках, потому что это не потокобезопасно, вместо этого вы должны использовать ConcurretBag. Кроме того, у List нет Append, у него есть Add. Это расширение?   -  person Magnetron    schedule 09.07.2018
comment
По крайней мере, вы могли бы ускорить свою первую реализацию, используя StringBuilder вместо конкатенации строк.   -  person Renatas M.    schedule 09.07.2018
comment
Спасибо вам за все ваши комментарии. Сегодня я собираюсь просмотреть каждую из ваших рекомендаций, чтобы увидеть, как я вписываю это в свой код. Позже будет сделано обновление на основе этой работы и рассмотрены отдельные комментарии за это время.   -  person Helbo    schedule 10.07.2018
comment
вы правы в том, что мой список не является потокобезопасным:/большая оплошность с моей стороны. @Rup   -  person Helbo    schedule 10.07.2018
comment
@CamiloTerevinto Мне сказали избегать функциональности Excel, поскольку мы не можем гарантировать, что на машине, получающей файл Excel, установлена ​​программа Excel. и, как я понял, использование функциональности Excel зависит от наличия установленного Excel?   -  person Helbo    schedule 10.07.2018
comment
@elgonzo, вы правы, я должен был использовать потокобезопасную коллекцию. И меня не волнует порядок, поскольку каждая строка представляет собой строку из документа Excel.   -  person Helbo    schedule 10.07.2018
comment
время чтения документа excel в моем случае не имеет значения, так как я буду ограничен скоростью передачи в Интернете, документ попадет в мою программную память непосредственно из поста https. и я совершенно уверен, что время, необходимое для его чтения, менее актуально, чем время, необходимое для его записи из памяти в файл csv.   -  person Helbo    schedule 10.07.2018
comment
@magnus нравится твоя идея, как я написал в твоем ответе.   -  person Helbo    schedule 10.07.2018
comment
@Magnetron да, вы правы, это большая ошибка с моей стороны, я сейчас работаю над изменением этой части.   -  person Helbo    schedule 10.07.2018
comment
@ Сид, я сейчас посмотрю на это, спасибо, что заметили эту ошибку.   -  person Helbo    schedule 10.07.2018
comment
@Reniuz да, я, вероятно, должен был использовать StringBuilder, но быстрее ли это, чем запись непосредственно в файл?   -  person Helbo    schedule 10.07.2018
comment
@Helbo StringBuilder хранит все в памяти, и это быстрее, чем объединение строк, но в конце вы запишете все из памяти в файл. Так что в вашем случае лучшим решением будет запись в файл вместо сохранения в StringBuilder, а затем запись.   -  person Renatas M.    schedule 10.07.2018


Ответы (1)


Пара проблем с вашим кодом.

  1. strCSVData.Append(strRow) на самом деле ничего не добавляет в список, он возвращает новое перечисляемое с добавленным элементом.
  2. Если бы вы действительно сделали Add, это тоже не сработало бы, поскольку List не является потокобезопасным.
  3. Параллельное выполнение не будет обрабатывать элементы по порядку. (обязательно)

Я думаю, что основная проблема с вашим исходным (не параллельным циклом) заключается в построении strCSVData путем конкатенации. Поскольку строки неизменяемы, каждый раз необходимо создавать новую строку, и чем она больше, тем медленнее она становится. Я бы посоветовал вам открыть StreamWriter перед циклом и вместо этого писать прямо в него.

...
StreamWriter csvFile = new StreamWriter(strOutputFileName, false);
for (int sheet = 0; sheet < excelsheets.Tables.Count; sheet++)
{
...
  foreach (DataRow row in aSheet.Rows)
  {
      foreach (var column in row.ItemArray)
      {
           csvFile.WriteLine(column.ToString().Replace(",", "&comma;") + ",");
      }
  }
...
}
person Magnus    schedule 09.07.2018
comment
это интересный подход, я обязательно попробую его тогда. вы говорите, что мой список не является потокобезопасным. что бы вы предложили вместо списка? Могу ли я использовать тот же подход к написанию напрямую? или я бы рискнул написать каждую строку в одну и ту же строку одновременно? - person Helbo; 10.07.2018
comment
Изменение, которое вы предложили в исходном методе, значительно изменило время работы. теперь мы считаем миллисекунды вместо минут. почти думал, что мне не нужно делать параллель. Однако меня не волнует порядок каждой строки, только все данные, соответствующие каждой строке, связаны по строке. и если бы я мог получить хотя бы немного производительности, я действительно считаю, что параллель была бы быстрее. - person Helbo; 10.07.2018
comment
Я собираюсь добавить ваш ответ в качестве принятого к концу сегодняшнего дня, если кто-то не придумает революционно более быстрый подход, тогда ваш ответ будет лучшим. - person Helbo; 10.07.2018
comment
Выполнение чего-либо параллельно не всегда быстрее, поскольку обычно это усложняет обработку синхронизации. - person Magnus; 10.07.2018