1

Собственно, вопрос в заголовке.
Нормально ли использовать Parallel.ForEachAsync внутри другого Parallel.ForEachAsync?
Есть сгруппированные данные (10х10), нужно их параллельно обработать, друг от друга группы не зависят. Каждую группу надо параллельно тоже обработать.

Так вот, можно ли так делать, чем это грозит, какие могут быть последствия?

Код для примера:

await Parallel.ForEachAsync(Enumerable.Range(0, 10), async (x, ctx) => {
await Parallel.ForEachAsync(Enumerable.Range(0, 10), async (t, ct) =>
{
    Console.WriteLine($"element = {t} on {x}");

    await Task.Delay(Random.Shared.Next(100, 5000));
});

});

OwDafuq
  • 577
  • 1
    Делать так можно. Грозит тем, что может быть использовано потоков больше, чем ядер в системе, что в итоге может немного снизить производительность. – Alexander Petrov Mar 17 '23 at 13:39
  • Просто таких обработчиков (foreach внутри foreach'a) будет несколько, крутятся они в Quartz.NET (допустим 15 обработчиков, 10х10 блоков параллельно), выиграю ли я в скорости или наоборот? Как это можно спрогнозировать? – OwDafuq Mar 17 '23 at 13:53
  • Это ASP.NET или что-то клиентское? – Jagailo Mar 17 '23 at 15:07
  • Это не апи и не сайт, простой backend, который обрабатывает только данные из БД, извне ничего не принимает в себя – OwDafuq Mar 18 '23 at 14:07

1 Answers1

1

А есть ли смысл? Обычно такие штуки я делаю так:

private Task DoWork(int x, int t, CancellationToken ct)
{
    Console.WriteLine($"element = {t} on {x}");
    return Task.Delay(Random.Shared.Next(100, 5000), ct);
}
List<Task> tasks = new();
for (int x = 0; x < 10; x++)
{
    for (int t = 0; t < 10; t++)
    {
        tasks.Add(DoWork(x, t, ct));
    }
}
await Task.WhenAll(tasks);

Это даёт мне возможость быстро и гибко управлять процессом обработки, и творить более сложные распараллеливания операций, а не только "всё сразу".

Но ваш код тоже нормальный. Как удобнее, так и делайте. Но 2 вложенных ForEachAsync не позволят наложить общее ограничение на количество выполняемых операций одновременно, а простое добавление семафора в цикл - сможет.

aepot
  • 49,560
  • Спасибо, попробуем и такой вариант – OwDafuq Mar 18 '23 at 14:08
  • А как быть в ситуации, когда в первом цикле создается disposed object? После выполнения второго цикла нужно освободить ресурсы. Нормально ли будет сложить их тоже в list, а потом у всех вызвать Dispose? – OwDafuq Mar 20 '23 at 05:26
  • @OwDafuq возможно и нормально, сильно зависит от контекста и количества таких объектов – aepot Mar 20 '23 at 06:50
  • Объект - обертка над DbContext'ом в другом слое приложения (infrastructure), отдается интерфейсом клиенту в Application слой, внутри создается руками DbContext через строку подключения (к разным серверам и базам), в Dispose методе вызов dbContext.Dispose, объектов может быть от 1 до 100 (мб тут вариант переписать сам класс, чтобы он не создавал каждый раз новые DbContext'ы, а только менял строку подключения). – OwDafuq Mar 20 '23 at 07:45
  • @OwDafuq вероятно что-то типа пула контестов можно устроить, чтобы к одной базе было одно уникальное подключение и оно реюзалось, сам пул сделать IDisposable, и в нем диспозить всё что есть. То есть спавн подключений должен быть не на уровне цикла, а по независимой логике. – aepot Mar 20 '23 at 08:08
  • В "сервисе" моем просто сделать "Dictionary<ServerGroup, DbContext> cache" (e.g.) (где ServerGroup имеет 2 св-ва: servername, dbname), потом при запросах в этот сервис передавать ServerGroup в сервис, а дальше сам сервис ищет в уже созданных DbContext'ах (cache) по ключу ServerGroup и через него вызывать запросы? И в конце у Dispose метода моего сервиса пройтись по всему Dictionary и вызвать у всех DbContext'ов Dispose? – OwDafuq Mar 20 '23 at 09:02
  • @OwDafuq учтите только что словарь не является потокобезопасным. А так, решайте сами как именно лучше сделать. – aepot Mar 20 '23 at 09:05
  • ну, со словарем я как пример, потокобезопасность это понятно :) Спасибо, будем пробовать) – OwDafuq Mar 20 '23 at 10:01