Parallel.ForEach vs Task.Run и Task.WhenAll


158

В чем разница между использованием 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);

3
Я думаю, что второй фрагмент кода будет почти равен первому фрагменту, если вы используете Task.WaitAllвместо Task.WhenAll.
избегать

15
Также обратите внимание, что второй будет выполнять DoSomething ("s3") три раза и не даст тот же результат! stackoverflow.com/questions/4684320/…
Nullius

1
Возможный дубликат Parallel.ForEach vs Task.Factory.StartNew
Мохаммед

@Dan: обратите внимание, что версия 2 использует async / await, что означает, что это другой вопрос. Async / await была введена в VS 2012, через 1,5 года после написания возможного дублирующего потока.
Петтер Т

Ответы:


159

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

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

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

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

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

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

1
Отличный ответ, мне было интересно, не могли бы вы указать мне хороший материал для чтения по этой теме?
Димитар Димитров

@DimitarDimitrov Для общих вещей TPL, reedcopsey.com/series/parallelism-in-net4
Рид Копси

1
Моя конструкция Parallel.ForEach вызывала сбой моего приложения. Я выполнял некоторую тяжелую обработку изображений внутри него. Однако, когда я добавил Task.Run (() => Parallel.ForEach (....)); Перестал сбой. Вы можете объяснить, почему? Пожалуйста, обратите внимание, что я ограничиваю параллельные опции количеством ядер в системе.
monkeyjumps

3
Что если DoSomethingесть async void DoSomething?
Франческо Боницци

1
Как насчет async Task DoSomething?
Шон Маклин,

37

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

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

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

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

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

2
не должно ли это быть await Task.WhenAll(strings.Select(async s => await Task.Run(() => DoSomething(s)));? У меня были проблемы при возврате задач (а не ожидании), особенно когда usingдля удаления объектов использовались операторы типа like .
Мартин Колл

Мой вызов Parallel.ForEach вызывал сбой моего пользовательского интерфейса. Я добавил Task.Run (() => Parallel.ForEach (....)); к этому, и это решило сбой.
monkeyjumps

1

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

  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);

Таким образом, вы выполняете задачи, которые выполняются одна за другой, или когда все запускаются одновременно?
Виниций Гуальберто

Насколько я могу судить, все они запускаются, когда я вызываю "DoSomethingAsync ()". Тем не менее, ничто не блокирует их, пока не будет вызван WhenAll.
Крис М.

Вы имеете в виду, когда вызывается первый DoSomethingAsync ()?
Виниций Гуальберто

1
@Миро. Он будет заблокирован до первого ожидания DoSomethingAsync (), поскольку именно это будет передавать выполнение обратно в ваш цикл. Если это синхронно и вы возвращаете Задачу, весь код будет выполняться один за другим, и WhenAll будет ожидать завершения всех Задач
Саймон Белангер,

0

Я видел, что 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}]");
        }
    }
}

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

введите описание изображения здесь

Вывод:

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

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.