Pourquoi devrais-je préférer un simple «Wait Task.WhenAll» plutôt que plusieurs waits?

127

Au cas où je ne me soucierais pas de l'ordre d'achèvement des tâches et que j'aurais juste besoin de toutes les terminer, dois-je toujours utiliser await Task.WhenAllau lieu de plusieurs await? par exemple, est DoWork2ci - dessous une méthode préférée pour DoWork1(et pourquoi?):

using System;
using System.Threading.Tasks;

namespace ConsoleApp
{
    class Program
    {
        static async Task<string> DoTaskAsync(string name, int timeout)
        {
            var start = DateTime.Now;
            Console.WriteLine("Enter {0}, {1}", name, timeout);
            await Task.Delay(timeout);
            Console.WriteLine("Exit {0}, {1}", name, (DateTime.Now - start).TotalMilliseconds);
            return name;
        }

        static async Task DoWork1()
        {
            var t1 = DoTaskAsync("t1.1", 3000);
            var t2 = DoTaskAsync("t1.2", 2000);
            var t3 = DoTaskAsync("t1.3", 1000);

            await t1; await t2; await t3;

            Console.WriteLine("DoWork1 results: {0}", String.Join(", ", t1.Result, t2.Result, t3.Result));
        }

        static async Task DoWork2()
        {
            var t1 = DoTaskAsync("t2.1", 3000);
            var t2 = DoTaskAsync("t2.2", 2000);
            var t3 = DoTaskAsync("t2.3", 1000);

            await Task.WhenAll(t1, t2, t3);

            Console.WriteLine("DoWork2 results: {0}", String.Join(", ", t1.Result, t2.Result, t3.Result));
        }


        static void Main(string[] args)
        {
            Task.WhenAll(DoWork1(), DoWork2()).Wait();
        }
    }
}
éviter
la source
2
Et si vous ne savez pas vraiment combien de tâches vous devez effectuer en parallèle? Que faire si vous avez 1000 tâches à exécuter? Le premier ne sera pas très lisible await t1; await t2; ....; await tn=> le second est toujours le meilleur choix dans les deux cas
cuongle
Votre commentaire a du sens. J'essayais juste de clarifier quelque chose pour moi-même, lié à une autre question à laquelle j'ai récemment répondu . Dans ce cas, il y avait 3 tâches.
éviter

Réponses:

113

Oui, à utiliser WhenAllcar il propage toutes les erreurs à la fois. Avec l'attente multiple, vous perdez des erreurs si l'une des attentes précédentes est lancée.

Une autre différence importante est qu'il WhenAllattendra que toutes les tâches soient terminées, même en présence de pannes (tâches défectueuses ou annulées). Attendre manuellement dans l'ordre entraînerait une concurrence inattendue car la partie de votre programme qui veut attendre continuera en fait tôt.

Je pense que cela facilite également la lecture du code car la sémantique souhaitée est directement documentée dans le code.

usr
la source
9
"Car il propage toutes les erreurs à la fois" Pas si vous awaitson résultat.
svick
2
Quant à la question de savoir comment les exceptions sont gérées avec Task, cet article donne un aperçu rapide mais bon du raisonnement qui le sous-tend (et il se trouve qu'il prend également note des avantages de WhenAll par rapport à plusieurs attend): blogs .msdn.com / b / pfxteam / archive / 2011/09/28 / 10217876.aspx
Oskar Lindberg
5
@OskarLindberg, l'OP commence toutes les tâches avant d'attendre la première. Donc, ils fonctionnent simultanément. Merci pour le lien.
usr
3
@usr J'étais encore curieux de savoir si WhenAll ne fait pas des choses intelligentes comme conserver le même SynchronizationContext, pour écarter davantage ses avantages de la sémantique. Je n'ai trouvé aucune documentation concluante, mais en regardant l'IL, il y a évidemment différentes implémentations de IAsyncStateMachine en jeu. Je ne lis pas très bien IL, mais WhenAll semble au moins générer du code IL plus efficace. (Dans tous les cas, le seul fait que le résultat de WhenAll reflète l'état de toutes les tâches impliquées pour moi est une raison suffisante pour le préférer dans la plupart des cas.)
Oskar Lindberg
17
Une autre différence importante est que WhenAll attendra que toutes les tâches soient terminées, même si, par exemple, t1 ou t2 lève une exception ou sont annulés.
Magnus
28

Je crois comprendre que la principale raison de préférer Task.WhenAllplusieurs awaits est le «barattage» des performances / tâches: la DoWork1méthode fait quelque chose comme ceci:

  • commencer avec un contexte donné
  • enregistrer le contexte
  • attendre t1
  • restaurer le contexte d'origine
  • enregistrer le contexte
  • attendre t2
  • restaurer le contexte d'origine
  • enregistrer le contexte
  • attendre t3
  • restaurer le contexte d'origine

En revanche, DoWork2fait ceci:

  • commencer avec un contexte donné
  • enregistrer le contexte
  • attendez tous les t1, t2 et t3
  • restaurer le contexte d'origine

La question de savoir si cela est suffisamment important pour votre cas particulier dépend, bien sûr, du contexte (pardonnez le jeu de mots).

Marcel Popescu
la source
4
Vous semblez penser que l'envoi d'un message au contexte de synchronisation coûte cher. Ce n'est vraiment pas le cas. Vous avez un délégué qui est ajouté à une file d'attente, cette file d'attente sera lue et le délégué exécuté. Les frais généraux que cela ajoute sont honnêtement très faibles. Ce n'est pas rien, mais ce n'est pas grand non plus. Les dépenses liées aux opérations asynchrones réduiront ces frais généraux dans presque tous les cas.
Servy
D'accord, c'était juste la seule raison pour laquelle je pouvais penser à préférer l'un à l'autre. Eh bien, cela plus la similitude avec Task.WaitAll où le changement de thread est un coût plus important.
Marcel Popescu
1
@Servy Comme le souligne Marcel, cela dépend VRAIMENT. Si vous utilisez await sur toutes les tâches db par principe, par exemple, et que db se trouve sur la même machine que l'instance asp.net, il y a des cas où vous attendez un hit db qui est en mémoire dans l'index, moins cher que ce commutateur de synchronisation et le shuffle de threadpool. Il pourrait y avoir une victoire globale significative avec WhenAll () dans ce genre de scénario, donc ... cela dépend vraiment.
Chris Moschini le
3
@ChrisMoschini Il est impossible que la requête de base de données, même si elle touche une base de données située sur la même machine que le serveur, soit plus rapide que la surcharge de l'ajout de quelques délégués à une pompe de messages. Cette requête en mémoire va certainement encore être beaucoup plus lente.
Servy
Notez également que si t1 est plus lent et que t2 et t3 sont plus rapides, alors l'autre attend le retour immédiatement.
David Refaeli
18

Une méthode asynchrone est implémentée en tant que machine à états. Il est possible d'écrire des méthodes afin qu'elles ne soient pas compilées dans des machines à états, ce qui est souvent appelé une méthode asynchrone accélérée. Ceux-ci peuvent être implémentés comme ceci:

public Task DoSomethingAsync()
{
    return DoSomethingElseAsync();
}

Lors de l'utilisation, Task.WhenAllil est possible de conserver ce code accéléré tout en garantissant que l'appelant est capable d'attendre que toutes les tâches soient terminées, par exemple:

public Task DoSomethingAsync()
{
    var t1 = DoTaskAsync("t2.1", 3000);
    var t2 = DoTaskAsync("t2.2", 2000);
    var t3 = DoTaskAsync("t2.3", 1000);

    return Task.WhenAll(t1, t2, t3);
}
Lukazoïde
la source
7

(Avertissement: Cette réponse est tirée / inspirée du cours TPL Async d'Ian Griffiths sur Pluralsight )

Une autre raison de préférer WhenAll est la gestion des exceptions.

Supposons que vous ayez un bloc try-catch sur vos méthodes DoWork, et supposons qu'elles appelaient différentes méthodes DoTask:

static async Task DoWork1() // modified with try-catch
{
    try
    {
        var t1 = DoTask1Async("t1.1", 3000);
        var t2 = DoTask2Async("t1.2", 2000);
        var t3 = DoTask3Async("t1.3", 1000);

        await t1; await t2; await t3;

        Console.WriteLine("DoWork1 results: {0}", String.Join(", ", t1.Result, t2.Result, t3.Result));
    }
    catch (Exception x)
    {
        // ...
    }

}

Dans ce cas, si les 3 tâches lancent des exceptions, seule la première sera interceptée. Toute exception ultérieure sera perdue. Ie si t2 et t3 lèvent une exception, seul t2 sera intercepté; etc. Les exceptions de tâches suivantes ne seront pas observées.

Où comme dans WhenAll - si une ou toutes les tâches échouent, la tâche résultante contiendra toutes les exceptions. Le mot clé await renvoie toujours toujours la première exception. Donc, les autres exceptions sont encore effectivement inobservées. Une façon de surmonter ce problème consiste à ajouter une suite vide après la tâche WhenAll et à y placer l'attente. De cette façon, si la tâche échoue, la propriété de résultat lèvera l'exception d'agrégation complète:

static async Task DoWork2() //modified to catch all exceptions
{
    try
    {
        var t1 = DoTask1Async("t1.1", 3000);
        var t2 = DoTask2Async("t1.2", 2000);
        var t3 = DoTask3Async("t1.3", 1000);

        var t = Task.WhenAll(t1, t2, t3);
        await t.ContinueWith(x => { });

        Console.WriteLine("DoWork1 results: {0}", String.Join(", ", t.Result[0], t.Result[1], t.Result[2]));
    }
    catch (Exception x)
    {
        // ...
    }
}
David Refaeli
la source
6

Les autres réponses à cette question offrent des raisons techniques pour lesquelles il await Task.WhenAll(t1, t2, t3);est préférable. Cette réponse visera à la regarder d'un côté plus doux (auquel @usr fait allusion) tout en arrivant à la même conclusion.

await Task.WhenAll(t1, t2, t3); est une approche plus fonctionnelle, car elle déclare l'intention et est atomique.

Avec await t1; await t2; await t3;, rien n'empêche un coéquipier (ou peut-être même votre futur moi!) D'ajouter du code entre les awaitdéclarations individuelles . Bien sûr, vous l'avez compressé en une seule ligne pour accomplir cela, mais cela ne résout pas le problème. En outre, il est généralement mauvais dans une équipe d'inclure plusieurs instructions sur une ligne de code donnée, car cela peut rendre le fichier source plus difficile à scanner pour les yeux humains.

En termes simples, il await Task.WhenAll(t1, t2, t3);est plus facile à maintenir, car il communique votre intention plus clairement et est moins vulnérable aux bogues particuliers qui peuvent provenir de mises à jour bien intentionnées du code, ou même simplement de fusions qui ont mal tourné.

rarrarrarrr
la source