2

Задача. Считать файлы с папки, для каждого произвести некоторые действия, и результат для каждого файла записать в базу данных, которая, будем считать, находится на другом компьютере. Необходимо увеличить производительность алгоритма с помощью распараллеливания задачи между ядрами процессора. Также стоит отметить, что при работе алгоритма происходит вызов стороннего приложения и считывание результата, через коды возврата. Но это не единственное действие, выполняемое для получения результата.


Ход моих мыслей таков. Задача разбивается на три операции:

  1. чтение файла
  2. получение результата из прочитанного
  3. запись результата в БД

Распараллеливанию стоит подвергнуть лишь вторую операцию, потому что первая это вывод с дисковой подсистемы, третья --- работа с сетью, а вот вторая это в основном с процессором. Причем основное время тратится именно на вторую операцию, а запись в БД и чтение файла, выполняются намного быстрее.

Значит, заведу две очереди Producer-Consumer:

  1. производитель --- поток, выполняющий операцию чтения файлов из папки. Потребители --- потоки, получающие результат из прочитанного
  2. производители --- потоки, получающие результат из прочитанного. Потребитель --- поток, записывающий результат в БД

Первый вопрос, касается числа потоков-потребителей для первой очереди. В похожем вопросе Как лучше распараллелить задачу? было рекомендовано выставлять число потоков выполняющих основную работу равным количеству ядер процессора. Но не следует ли уменьшить это число, дабы "освободить" одно ядро для потоков первой и третьей операции?

Второй вопрос о том ньюансе, что операция получения результата включает вызов стороннего приложения. Не разрушит ли он эффективность распараллеливания задачи?

4per
  • 2,696
  • 1
    пока эффективным вариантом параллельной обработки я вижу организацию конвейера с параллельным выполнением этапов этого конвейера. Ну запись в БД на самом деле тоже можно дополнительно распараллелить. – rdorn Dec 03 '16 at 13:38
  • Чтение с диска сводится в-основном к ожиданию IO. То есть первая операция будет в-основном ждать. Вторая операция - тут всё зависит от стороннего приложения. Может оно само многопоточное? И вообще, тут потоки вряд ли нужны: следует запускать несколько (по количеству ядер) экземпляров того приложения, а не несколько потоков/задач. Третья операция: тут опять всё сводится к ожиданию запроса к БД. Если использовать асинхронный API. то всё делается в одном потоке. – Alexander Petrov Dec 04 '16 at 07:31
  • @AlexanderPetrov, вызов стороннего приложения - не единственное из чего состоит вторая операция. – 4per Dec 04 '16 at 10:48
  • @rdorn, не очень понял для чего распараллеливать запись в БД. А в остальном, я вроде всё-так и написал? – 4per Dec 04 '16 at 10:50
  • 1
    @4per если запись результата подразумевает выполнение одного запроса, то конечно ничего параллелить не нужно, а если запросов несколько и они не завязаны друг на друга, то их можно запустить параллельно, это ускорит работу. – rdorn Dec 04 '16 at 16:12
  • Сколько данных предполагается записывать в БД? Если много, то лучше передавать их не по-одной, а пакетами. В MS SQL Server это делается через хранимую процедуру, принимающую табличный параметр. – Pavel Mayorov Dec 06 '16 at 06:15
  • 1
    "число потоков выполняющих основную работу равным количеству ядер процессора" - следует понимать, что если основная работа требует какого-либо ответа, т.е. основную часть времени будет ожидать, то число потоков можно безболезненно увеличивать. – Trymount Dec 06 '16 at 07:05
  • @PavelMayorov, данных не много 1-200 строк по одной на файл. Обращение к БД в один момент, конечно вариант, реализовать можно разными способами. Мой вопрос о настройках потоков для основной операции. – 4per Dec 06 '16 at 07:47
  • @Trymount, я вас не совсем понял. Основная операция будет ожидать разное время, может меньшее, может большее от своего времени жизни. Увеличивать число потоков насколько? – 4per Dec 06 '16 at 07:49
  • 1
    @4per это можно выяснить опытным путём. Ну, например, если у вас идет запрос к сайту. Ожидать от него ответа можно 2-10секунд. Если у вас число потоков равно числу ядер, то каждый поток будет простаивать по 2-10 секунд. Если увеличивать число потоков, то время простоя будет уменьшаться. Но при этом нужно понимать, что начинают тратиться ресурсы на переключение между потоками. При подборе количества потоков, запустите диспетчер задач и смотрите на загруженность цп. Если >80-90%, то скорее всего стоит остановиться. – Trymount Dec 06 '16 at 08:04
  • @4per Я слегка не понял, в чем проблема написать что то типа SaveToDb( ReadAllLines().AsParallel().Select(x=>GetResult()).ToArray() ) , если строк будет всего 200 штук? – tym32167 Dec 09 '16 at 10:57
  • @tym32167 AsParallel() сам подберёт оптимальное кол-во потоков? а как скажется вызовы стороннего приложения? Возможно будет в вашем варианте реализовать сообщения пользователю о прогрессе? Не будет ли проблем с использованием оперативной памяти, т.е. как должен быть реализован ReadAllLines() ? – 4per Dec 10 '16 at 04:12
  • @4per Если вам нужно управлять количеством потоков, то можно использовать WithDegreeOfParallelism. Как напишете код, так и скажется, я не знаю как и что вы вызываете. Аналогично и про сообщение о прогрессе - будет работать только так, как вы организуете. По поводу ReadAllLines - вы писали, что там всего 200 строк, так что зависит от длины этих строк, если они небольшие - то все ок, если каждая по 50 гигов, то все плохо будет. – tym32167 Dec 10 '16 at 07:40
  • @tym32167 до 200 строк надо сохранить в БД, соответственно 200 файлов из папки, каждый по сколько-то Мб, так что хранить их все сразу в памяти не стоит, с учетом запаса. Сторонний процесс вызываю просто process.Start(); process.WaitForExit(timeout); – 4per Dec 10 '16 at 11:20
  • @4per Так вы уже сами знаете ответы на все важные вопросы, осталось начать писать код :) – tym32167 Dec 10 '16 at 12:35

2 Answers2

2

Если вы не знаете где будет работать программа, в каких условиях и на каком железе, но лишь примерно знаете что вот - этот этап можно распараллелить, то для выяснения оптимального количества потоков следует проводить бенчмарк.

Стратегий для такого бенчмарка есть несколько.

  1. Проводить бенчмарк при первом, или каждом, запуске, фиксируя оптимальное количества потоков. Здесь можно сберечь время если сохранять оптимальное значение для известных характеристик железа - объёма памяти и процессора - пересчитывая оптимум только если железо поменялось.

  2. Если эксклюзивное использование железа не подразумевается, а значит доступные вычислительные ресурсы не являются константой, то оптимальней будет динамически определять оптимальное число потоков для всех процедур. Примерный алгоритм: запускаем один процесс, записываем время выполнения. Запускаем второй - записываем время. Смотрим разницу - не увеличилось? Запускаем ещё один процесс. Снова смотрим время. Запускаем ещё. Измеряем снова. Увеличилось? Стоп, лишний процесс убираем. Через некоторое время снова пытаемся запустить один дополнительный процесс.

Понятно что стратегии можно и нужно комбинировать. Например, если вы точно знаете что на 16 Гб памяти больше чем 8 процессов не имеет смысла запускать, то в динамическом алгоритме на 8 процессах стоит остановиться.

sanmai
  • 12,320
  • "ясно что вы не знаете" знаю, больше всего занимает обработка, для неё скорость ядра процессора, является ограничителем, причем это верно и для обработки в моём коде и в вызываемом стороннем приложении. Чтение с диска и запись в БД занимают много меньше времени, но хранить все файлы в памяти нельзя. – 4per Dec 07 '16 at 01:38
  • Дополните вопрос этим пожалуйста. Здорово было бы попутно удалить из него всё не относящееся к сути вопроса. – sanmai Dec 07 '16 at 01:49
  • А что не относится к сути вопроса? – 4per Dec 07 '16 at 01:53
  • 1
    Всё что не является конкретным. Вопросы должны быть четкими. В вопросах должны быть показаны неподходящие варианты решения. Из них должно быть видно что вы провели всю подготовительную работу, но не нашли решения. Из вашего вопроса сейчас непонятно - пытались ли вы как-то решить вашу проблему, и как? Что не получилось?.. Пока видны ваши размышления, но они не являются сами по себе попыткой практически решить задачу. – sanmai Dec 07 '16 at 01:55
  • Если б я что считал не конкретным, я бы не добавлял это в вопрос. Это не значит, что я не ошибаюсь. Но сейчас я еще раз просмотрел, свой вопрос и всё ещё не вижу не конкретного. Так же в моём вопросе присутствует "Поделитесь результатом вашего поиска и расскажите, что вы нашли и почему найденные ответы вас не устроили." и план моего решения. "Что не получилось?" - я не знаю, как настроить правильно количество потоков для основной операции, и не знаю как на это влияет вызов стороннего приложения. – 4per Dec 07 '16 at 02:01
  • Что вы сделали чтобы узнать то, что вы не знаете? Как вы считаете другие люди могут это узнать если у них нет ни примеров кода, ни вашей этой программы, ничего кроме общих соображений? – sanmai Dec 07 '16 at 02:05
2

Если интерес академический или вы точно знаете, для какого железа это будет работать, и у вас есть доступ к этому железу - то, возможно, есть смысл проводить бенчмарки. Но если это задача для какого то реального приложения, то не вижу смысла. Сегодня приложение запустят на слабой машине, завтра на сильной, послезавтра закупят новые процессоры и все изменится. Потому не вижу смысла не доверять стандартному шедулеру и выдумывать велосипеды. Предлагаю поглядеть на следующий очень простой псевдокод

void Main()
{
    var files = Enumerable.Range(0, 100).Select(x => 
    {
        var fname = $"{x}.txt";
        Console.WriteLine($"Reading {fname}");
        return fname;
    });             

    WriteToDb(files.AsParallel().Select(x=>Process(x)));

    // Если хочется начать писать в БД до того, как все файлы будут обработаны. Я бы выбрал этот вариант
    //WriteToDb(files.AsParallel().WithMergeOptions(ParallelMergeOptions.NotBuffered).Select(x=>Process(x)));

    // Если уж сильно хочется контролить количество потоков. Не советую этого делать, лучше побеспокойтесь о памяти.
    // WriteToDb(files.AsParallel().WithDegreeOfParallelism(Environment.ProcessorCount).Select(x=>Process(x)));             
}   

string Process(string fname)
{
    Console.WriteLine($"processing {fname}");

    for (var i = 0; i < 1000000000; i++)
    {       
    }   

    return $"{fname} processed";
}

void WriteToDb(IEnumerable<string> results)
{
    Console.WriteLine("Start writing to DB");
    foreach (var result in results)
    {
        Console.WriteLine($"Saving {result}");
    }
}

Обратите внимание на загрузку процессоров, я уверен, что она поползет вверх до 100% всеми процессорами/ядрами. Это не только проще, чем синхронизировать все вручную, это ещё (в теории) должно быть эффективней ручного создания кучи потоков, так как стандарнтый шедулер может переиспользовать уже созданные потоки в пуле.

UPD. Дополню, что происходит.

WriteToDb(files.AsParallel().Select(x=>Process(x)));

Процесс такой: читаем параллельно несколько файлов, обрабатываем параллельно, складываем результат. Когда все результаты по всем файлам готовы (или не все - решает PLINQ) - пишем в БД. Количество параллельно работающих потоков определяет шедулер.

// Если хочется начать писать в БД до того, как все файлы будут обработаны. Я бы выбрал этот вариант
//WriteToDb(files.AsParallel().WithMergeOptions(ParallelMergeOptions.NotBuffered).Select(x=>Process(x)));

Процесс такой: читаем параллельно несколько файлов, обрабатываем параллельно, результаты параллельно пишем в БД. Количество параллельно работающих потоков определяет шедулер.

// Если уж сильно хочется контролить количество потоков. Не советую этого делать, лучше побеспокойтесь о памяти.
// WriteToDb(files.AsParallel().WithDegreeOfParallelism(Environment.ProcessorCount).Select(x=>Process(x))); 

Процесс такой: читаем параллельно несколько файлов, обрабатываем параллельно, складываем результат. Когда все результаты по всем файлам готовы (или не все - решает PLINQ) - пишем в БД. Количество параллельно работающих заданы тут WithDegreeOfParallelism.

Также эти варианты можно комбинировать.

tym32167
  • 32,857
  • Я правильно понял ваш код? читаем порцию, обрабатываем, читаем порцию, обрабатываем. Размеры порции зависят от того, сколько потоков выделит Parallel LINQ. Через каждые сотню делаем остановку для записи в БД. – 4per Dec 10 '16 at 13:27
  • 1
    @4per читаем порцию, обрабатываем, читаем порцию, обрабатываем. Размеры порции зависят от того, сколько потоков выделит Parallel LINQ - ДА, Через каждые сотню делаем остановку для записи в БД - нет, неправильно. Обновил ответ. – tym32167 Dec 10 '16 at 13:36