TPL Dataflow против простого семафора

У меня есть требование сделать процесс масштабируемым. В процессе в основном выполняются операции ввода-вывода с некоторыми второстепенными операциями ЦП (в основном десериализация строк). Процесс запрашивает в базе данных список URL-адресов, затем извлекает данные из этих URL-адресов, десериализует загруженные данные в объекты, затем сохраняет некоторые данные в динамике crm, а также в другой базе данных. После этого мне нужно обновить первую базу данных, URL-адреса которой были обработаны. Частично требование - сделать степень параллелизма настраиваемой.

Изначально я думал реализовать это через последовательность задач с ожиданием и ограничить параллелизм с помощью семафора - довольно просто. Затем я прочитал здесь несколько сообщений и ответов @Stephen Cleary, который рекомендует использовать TPL Dataflow, и я подумал, что это может быть хорошим кандидатом. Однако я хочу быть уверенным, что «усложняю» код, используя Dataflow по уважительной причине. Я также получил предложение использовать метод расширения ForEachAsync, который также прост в использовании, однако я не уверен, не вызовет ли он накладных расходов памяти из-за того, как он разбивает коллекцию на разделы.

Подходит ли TPL Dataflow для такого сценария? Чем он лучше, чем метод Semaphore или ForEachAsync - какие преимущества я получу, если реализую его через TPL DataFlow по сравнению с другими вариантами (Semaphore / ForEachASync)?


person BornToCode    schedule 31.07.2018    source источник
comment
Tpl Dataflow лучше подходит для работы с процессором. Для вызовов асинхронного ввода-вывода я бы использовал Task.WhenAll с рядом задач.   -  person Peter Bons    schedule 31.07.2018
comment
@PeterBons - В моем сценарии в основном используются вызовы ввода-вывода, но также есть небольшая работа с процессором (например, десериализация содержимого файлов), я мог бы просто реализовать его с помощью семафора, но у меня сложилось впечатление, что я бы увеличил производительность, используя поток данных Tpl , но я все еще не уверен, что полностью понимаю преимущества Dataflow, поэтому я мог определить, стоят ли они того, потому что это, вероятно, сделало бы мой код более сложным, чем просто использование семафора?   -  person BornToCode    schedule 31.07.2018
comment
Мне действительно интересно узнать мнение экспертов по этому поводу. Я делаю то же самое, что и вы, и не могу выбрать между семафором и потоком данных TPL. Я склоняюсь к использованию ActionBlock с MaxDegreeOfParallelism в качестве настраиваемого. Насколько я понимаю, TPL эффективно управляет пулом потоков, но есть некоторые другие проблемы. Я хочу, чтобы это было просто, просто ограничьте количество задач, выполняемых одновременно, вы тоже этим занимаетесь?   -  person Polynomial Proton    schedule 31.07.2018
comment
о, кстати, ознакомьтесь с этим ответом от @Stephen Cleary. TPL Dataflow is great, especially if you're looking to limit work in one part of a larger pipeline Однако, если дросселируемое действие нужно выполнить только один раз, то семафора достаточно.   -  person Polynomial Proton    schedule 31.07.2018
comment
@TheUknown - Хорошие новости, мы получили ответ от эксперта :) Моя цель - не просто ограничить количество задач, но и сделать так, чтобы весь процесс завершился как можно быстрее, зная, что часть, которая пишет в Crm это главное узкое место. Спасибо за ссылку на свой комментарий к другому ответу, он также информативен и соответствует моей ситуации.   -  person BornToCode    schedule 01.08.2018


Ответы (2)


В процессе в основном выполняются операции ввода-вывода с некоторыми второстепенными операциями ЦП (в основном десериализация строк).

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

Итак, вам нужно сосредоточиться на параллельной асинхронности.

  • SemaphoreSlim - это стандартный шаблон для этого, как вы обнаружили.
  • TPL Dataflow также может выполнять параллелизм (как в асинхронной, так и в параллельной формах).

ForEachAsync может принимать несколько форм; обратите внимание, что в сообщении блога , на которые вы ссылались, существует 5 различных реализаций этого метода, каждая из которых допустима. «[T] здесь существует множество различных семантик, возможных для итерации, и каждая из них приведет к разным вариантам дизайна и реализации». Для ваших целей (не требующих распараллеливания ЦП) вам не следует рассматривать те, которые используют Task.Run или разбиение на разделы. В мире асинхронного параллелизма любая реализация ForEachAsync будет просто синтаксическим сахаром, скрывающим, какую семантику она реализует, поэтому я стараюсь ее избегать.

Остается SemaphoreSlim против ActionBlock. Обычно я рекомендую людям начинать с SemaphoreSlim и подумать о переходе на TPL Dataflow, если их потребности станут более сложными (таким образом, что кажется, что они выиграют от конвейера потока данных).

Например, «Частью требования является возможность настройки степени параллелизма».

Вы можете начать с разрешения некоторой степени параллелизма - когда то, что регулируется, представляет собой единую целую операцию (выборка данных из URL-адреса, десериализация загруженных данных в объекты, сохранение в динамике crm и в другой базе данных и обновление первой базы данных). Вот где SemaphoreSlim было бы идеальным решением.

Но вы можете решить, что хотите иметь несколько регуляторов: скажем, одну степень параллелизма для количества загружаемых URL-адресов, отдельную степень параллелизма для сохранения и отдельную степень параллелизма для обновления исходной базы данных. И тогда вам также нужно будет ограничить «очереди» между этими точками: только определенное количество десериализованных объектов в памяти и т. Д. - чтобы быстрые URL-адреса с медленными базами данных не вызывали проблем с вашим приложением, использующим слишком много объем памяти. Если это полезная семантика, значит, вы начали подходить к проблеме с точки зрения потока данных, и именно поэтому вам может быть лучше подойдет такая библиотека, как TPL Dataflow.

person Stephen Cleary    schedule 31.07.2018
comment
Большое спасибо за этот ответ, он подробный и ясный, и вы даже упомянули все упомянутые мной параметры, включая ForEachAsync! +100 :) - person BornToCode; 01.08.2018

Вот преимущества подхода семафор:

  1. Простота

И вот преимущества подхода TPL Dataflow:

  1. Параллелизм задач поверх параллелизма данных
  2. Оптимальное использование ресурсов (пропускная способность, ЦП, подключения к базе данных)
  3. Настраиваемая степень параллелизма для каждой разнородной операции
  4. Уменьшение объема памяти

Рассмотрим, например, следующую реализацию семафора:

string[] urls = FetchUrlsFromDB();
var cts = new CancellationTokenSource();
var semaphore = new SemaphoreSlim(10); // Degree of parallelism (DOP)
Task[] tasks = urls.Select(url => Task.Run(async () =>
{
    await semaphore.WaitAsync(cts.Token);
    try
    {
        string rawData = DownloadData(url);
        var data = Deserialize(rawData);
        PersistToCRM(data);
        MarkAsCompleted(url);
    }
    finally
    {
        semaphore.Release();
    }
})).ToArray();
Task.WaitAll(tasks);

Вышеупомянутая реализация гарантирует, что в любой момент одновременно будет обрабатываться не более 10 URL-адресов. Однако координации между этими параллельными рабочими процессами не будет. Так, например, вполне возможно, что в данный момент все 10 параллельных рабочих процессов будут загружать данные, в другой момент все 10 будут десериализовать необработанные данные, а в другой момент все 10 будут сохранять данные в CRM. Это далеко не идеально. В идеале вы хотели бы, чтобы узкое место всей операции, будь то сетевой адаптер, ЦП или сервер базы данных, работало все время без остановки и не использовалось недостаточно (или полностью не использовалось) в различные случайные моменты.

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

Если количество URL-адресов очень велико, скажем, 1000000, то подход с использованием семафоров, описанный выше, также вызывает серьезные проблемы использования памяти. URL-адрес может иметь размер в среднем 50 байт, а Task, подключенный к CancellationToken, может быть в 10 раз тяжелее или больше. Конечно, вы можете изменить реализацию и использовать _4 _ более умным способом, который не создает так много задач, но это будет противоречить основному (и единственному) аргументу этого подхода - его простоте.

Библиотека TPL Dataflow решает все эти проблемы за счет (небольшой) кривой обучения, необходимой для того, чтобы иметь возможность приручить этот мощный инструмент.

person Theodor Zoulias    schedule 11.06.2020