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
Я думаю, що фрагмент 2-го коду був би майже рівний першому, якщо ви використовували його Task.WaitAllзамість Task.WhenAll.
Avo

15
Зауважте також, що другий виконає DoSomething ("s3") три рази, і це не дасть однакового результату! stackoverflow.com/questions/4684320 / ...
Nullius

1
Можливий дублікат Parallel.ForEach vs Task.Factory.StartNew
Mohammad

@Dan: зауважте, що Версія 2 використовує функцію async / await, а це означає, що це вже інше питання. Async / await був представлений з VS 2012, через 1,5 року після написання можливої ​​копії теми.
Петтер Т

Можливо, подивіться на .net Core Parallel.ForEach issues
Heretic Monkey

Відповіді:


159

У цьому випадку другий метод асинхронно чекатиме завершення завдань замість блокування.

Однак є недолік для використання Task.Runв циклі - With 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
Reed

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були залучені для розпорядження об'єктами.
Мартін Колл

Мій виклик 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);

Таким чином ви виконуєте Завдання, які виконуються одна за одною, або коли AllAll запускає їх усі відразу?
Вініцій Гуальберто

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

Ви маєте на увазі, коли викликається перший "DoSomethingAsync ()"?
Vinicius Gualberto

1
@ChrisM. Він буде заблокований до першого очікування 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.