Parallel.ForEach vs Task.Run et Task.WhenAll

158

Quelles sont les différences entre l'utilisation de Parallel.ForEach ou de Task.Run () pour démarrer un ensemble de tâches de manière asynchrone?

Version 1:

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

Version 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);
Petter T
la source
3
Je pense que le 2ème fragment de code serait presque égal au 1er si vous utilisiez à la Task.WaitAllplace de Task.WhenAll.
éviter
15
Veuillez également noter que le second exécutera DoSomething ("s3") trois fois et qu'il ne produira pas le même résultat! stackoverflow.com/questions/4684320/…
Nullius
1
Copie
Mohammad
@Dan: notez que la version 2 utilise async / await, ce qui signifie que c'est une question différente. Async / await a été introduit avec VS 2012, 1,5 an après l'écriture du thread en double possible.
Petter T
Pourrait jeter un oeil à .net Core Parallel.ForEach issues
Heretic Monkey

Réponses:

159

Dans ce cas, la deuxième méthode attendra de manière asynchrone que les tâches se terminent au lieu de se bloquer.

Cependant, il y a un inconvénient à utiliser Task.Rundans une boucle - Avec Parallel.ForEach, il y a un Partitionerqui est créé pour éviter de faire plus de tâches que nécessaire. Task.Runfera toujours une seule tâche par élément (puisque vous faites cela), mais les Parallellots de classe fonctionnent donc vous créez moins de tâches que le nombre total d'éléments de travail. Cela peut fournir des performances globales nettement meilleures, en particulier si le corps de la boucle a une petite quantité de travail par élément.

Si tel est le cas, vous pouvez combiner les deux options en écrivant:

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

Notez que cela peut également être écrit sous cette forme plus courte:

await Task.Run(() => Parallel.ForEach(strings, DoSomething));
Reed Copsey
la source
1
Excellente réponse, je me demandais si vous pouviez m'indiquer un bon matériel de lecture sur ce sujet?
Dimitar Dimitrov
@DimitarDimitrov Pour des informations générales sur TPL, reedcopsey.com/series/parallelism-in-net4
Reed Copsey
1
Ma construction Parallel.ForEach plantait mon application. J'effectuais un traitement d'image lourd à l'intérieur. Cependant, quand j'ai ajouté Task.Run (() => Parallel.ForEach (....)); Il a cessé de s'écraser. Pouvez-vous expliquer pourquoi? Veuillez noter que je limite les options parallèles au nombre de cœurs sur le système.
monkeyjumps
3
Et si DoSomethingc'est async void DoSomething?
Francesco Bonizzi
1
Et quoi async Task DoSomething?
Shawn Mclean
37

La première version bloquera de manière synchrone le thread appelant (et exécutera certaines des tâches dessus).
S'il s'agit d'un thread d'interface utilisateur, cela gèlera l'interface utilisateur.

La deuxième version exécutera les tâches de manière asynchrone dans le pool de threads et libérera le thread appelant jusqu'à ce qu'elles soient terminées.

Il existe également des différences dans les algorithmes de planification utilisés.

Notez que votre deuxième exemple peut être raccourci en

await Task.WhenAll(strings.Select(s => Task.Run(() => DoSomething(s)));
SLaks
la source
2
ça ne devrait pas être await Task.WhenAll(strings.Select(async s => await Task.Run(() => DoSomething(s)));? J'ai eu des problèmes lors du retour de tâches (au lieu d'attendre), en particulier lorsque des instructions comme usingétaient impliquées pour éliminer des objets.
Martín Coll
Mon appel Parallel.ForEach provoquait le crash de mon interface utilisateur J'ai ajouté Task.Run (() => Parallel.ForEach (....)); à lui et il a résolu le crash.
monkeyjumps
1

J'ai fini par faire ça, car c'était plus facile à lire:

  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);
Chris M.
la source
De cette façon, les tâches sont exécutées les unes après les autres ou WhenAll les démarre toutes en même temps?
Vinicius Gualberto
Autant que je sache, ils ont tous commencé lorsque j'appelle "DoSomethingAsync ()". Rien ne les bloque, cependant, jusqu'à ce que le WhenAll soit appelé.
Chris M.
Vous voulez dire quand le premier "DoSomethingAsync ()" est appelé?
Vinicius Gualberto
1
@ChrisM. Il sera bloqué jusqu'à la première attente de DoSomethingAsync () car c'est ce qui transférera l'exécution vers votre boucle. Si c'est synchrone et que vous retournez une tâche, tout le code sera exécuté l'un après l'autre et le WhenAll attendra que toutes les tâches soient terminées
Simon Bélanger
0

J'ai vu Parallel.ForEach utilisé de manière inappropriée, et j'ai pensé qu'un exemple dans cette question serait utile.

Lorsque vous exécutez le code ci-dessous dans une application console, vous verrez comment les tâches exécutées dans Parallel.ForEach ne bloquent pas le thread appelant. Cela peut convenir si vous ne vous souciez pas du résultat (positif ou négatif), mais si vous avez besoin du résultat, vous devez vous assurer d'utiliser 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}]");
        }
    }
}

Voici le résultat:

entrez la description de l'image ici

Conclusion:

L'utilisation de Parallel.ForEach avec une tâche ne bloquera pas le thread appelant. Si vous vous souciez du résultat, assurez-vous d'attendre les tâches.

~ Acclamations

Rogala
la source