Параллельный .ForEach vs Task.Run и Task.WhenAll

В чем разница между использованием Parallel.ForEach или Task.Run() для асинхронного запуска набора задач?

Версия 1:

List<string> strings = new List<string> { "s1", "s2", "s3" };
Parallel.ForEach(strings, s =>
{
    DoSomething(s);
});

Версия 2:

List<string> strings = new List<string> { "s1", "s2", "s3" };
List<Task> Tasks = new List<Task>();
foreach (var s in strings)
{
    Tasks.Add(Task.Run(() => DoSomething(s)));
}
await Task.WhenAll(Tasks);

Ответ 1

В этом случае второй метод будет асинхронно ждать завершения задач вместо блокировки.

Однако существует недостаток в использовании Task.Run в цикле. С Parallel.ForEach существует Partitioner, который создается, чтобы не создавать больше задач, чем это необходимо. Task.Run всегда будет делать одну задачу для каждого элемента (так как вы это делаете), но пакеты классов Parallel работают, поэтому вы создаете меньше задач, чем общие рабочие элементы. Это может обеспечить значительно лучшую общую производительность, особенно если тело цикла имеет небольшой объем работы для каждого элемента.

Если это так, вы можете объединить оба параметра, написав:

await Task.Run(() => Parallel.ForEach(strings, s =>
{
    DoSomething(s);
}));

Обратите внимание, что это также можно записать в этой более короткой форме:

await Task.Run(() => Parallel.ForEach(strings, DoSomething));

Ответ 2

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

Вторая версия будет запускать задачи асинхронно в пуле потоков и освобождать вызывающий поток до тех пор, пока они не будут выполнены.

Существуют также различия в используемых алгоритмах планирования.

Обратите внимание, что ваш второй пример можно сократить до

await Task.WhenAll(strings.Select(s => Task.Run(() => DoSomething(s)));

Ответ 3

Я закончил тем, что сделал это, поскольку это было легче читать:

  List<Task> x = new List<Task>();
  foreach(var s in myCollectionOfObject)
  {
      // Note there is no await here. Just collection the Tasks
      x.Add(s.DoSomethingAsync());
  }
  await Task.WhenAll(x);

Ответ 4

Я видел, что Parallel.ForEach использовался ненадлежащим образом, и я подумал, что пример в этом вопросе поможет.

Когда вы запустите приведенный ниже код в консольном приложении, вы увидите, как задачи, выполняемые в Parallel.ForEach, не блокируют вызывающий поток. Это может быть хорошо, если вы не заботитесь о результате (положительном или отрицательном), но если вам нужен результат, вы должны обязательно использовать Task.WhenAll.

using System;
using System.Linq;
using System.Threading.Tasks;

namespace ParrellelEachExample
{
    class Program
    {
        static void Main(string[] args)
        {
            var indexes = new int[] { 1, 2, 3 };

            RunExample((prefix) => Parallel.ForEach(indexes, (i) => DoSomethingAsync(i, prefix)),
                "Parallel.Foreach");

            Console.ForegroundColor = ConsoleColor.Yellow;
            Console.WriteLine("*You'll notice the tasks haven't run yet, because the main thread was not blocked*");
            Console.WriteLine("Press any key to start the next example...");
            Console.ReadKey();

            RunExample((prefix) => Task.WhenAll(indexes.Select(i => DoSomethingAsync(i, prefix)).ToArray()).Wait(),
                "Task.WhenAll");
            Console.WriteLine("All tasks are done.  Press any key to close...");
            Console.ReadKey();
        }

        static void RunExample(Action<string> action, string prefix)
        {
            Console.ForegroundColor = ConsoleColor.White;
            Console.WriteLine($"{Environment.NewLine}Starting '{prefix}'...");
            action(prefix);
            Console.WriteLine($"{Environment.NewLine}Finished '{prefix}'{Environment.NewLine}");
        }


        static async Task DoSomethingAsync(int i, string prefix)
        {
            await Task.Delay(i * 1000);
            Console.WriteLine($"Finished: {prefix}[{i}]");
        }
    }
}

Вот результат:

enter image description here

Заключение:

Использование Parallel.ForEach с задачей не блокирует вызывающий поток. Если вам небезразличен результат, обязательно дождитесь выполнения заданий.

~ Приветствия