202 votes

Pourquoi devrais-je préférer un seul 'await Task.WhenAll' à plusieurs awaits ?

Si je ne me préoccupe pas de l'ordre d'achèvement des tâches et que j'ai juste besoin qu'elles soient toutes achevées, dois-je quand même utiliser l'option await Task.WhenAll au lieu de plusieurs await ? par exemple, est DoWork2 sous 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();
        }
    }
}

181voto

usr Points 74796

Oui, utilisez WhenAll car il propage toutes les erreurs en une seule fois. Avec les attentes multiples, vous perdez les erreurs si l'une des attentes précédentes est lancée.

Une autre différence importante est que WhenAll attendra que toutes les tâches soient terminées même en présence d'échecs (tâches défaillantes ou annulées). Le fait d'attendre manuellement dans l'ordre provoquerait une concurrence inattendue car la partie de votre programme qui veut attendre va en fait continuer plus tôt.

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

38voto

Marcel Popescu Points 1174

Je crois savoir que la principale raison de préférer Task.WhenAll à plusieurs await s est le "barattage" des performances / des tâches : la DoWork1 fait quelque chose comme ça :

  • commencer avec un contexte
  • sauvegarder le contexte
  • attendre t1
  • rétablir le contexte original
  • sauvegarder le contexte
  • attendre t2
  • rétablir le contexte original
  • sauvegarder le contexte
  • attendre t3
  • rétablir le contexte original

En revanche, DoWork2 fait ça :

  • partir d'un contexte donné
  • sauvegarder le contexte
  • attendre la totalité de t1, t2 et t3
  • rétablir le contexte original

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

24voto

Lukazoid Points 6577

Une méthode asynchrone est mise en œuvre comme une machine à états. Il est possible d'écrire des méthodes de manière à ce qu'elles ne soient pas compilées en machines à états, ce que l'on appelle souvent une méthode asynchrone rapide. Ces méthodes peuvent être implémentées comme suit :

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

Lorsque vous utilisez Task.WhenAll il est possible de maintenir ce code rapide tout en garantissant à l'appelant la possibilité d'attendre la fin de toutes les tâches, 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);
}

13voto

David Refaeli Points 732

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

Une autre raison de préférer WhenAll est le traitement des exceptions.

Supposons que vous ayez un bloc try-catch sur vos méthodes DoWork, et supposons qu'elles appellent 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 rattrapée. Toute exception ultérieure sera perdue. C'est-à-dire que si t2 et t3 lèvent une exception, seule t2 sera attrapée ; etc. Les exceptions des tâches suivantes ne seront pas observées.

Alors que dans le cas de WhenAll, si l'une ou l'ensemble des tâches échoue, la tâche résultante contiendra toutes les exceptions. Le mot-clé await relance toujours la première exception. Ainsi, les autres exceptions ne sont toujours pas observées. Une façon de surmonter ce problème est d'ajouter une continuation vide après la tâche WhenAll et d'y placer le mot-clé await. De cette façon, si la tâche échoue, la propriété du résultat lancera l'exception complète de l'agrégat :

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)
    {
        // ...
    }
}

11voto

rarrarrarrr Points 53

Les autres réponses à cette question proposent des raisons techniques pour lesquelles await Task.WhenAll(t1, t2, t3); est préférable. Cette réponse aura pour but d'aborder la question sous un angle 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.

Con await t1; await t2; await t3; il n'y a rien qui empêche un coéquipier (ou peut-être même votre futur vous-même !) d'ajouter du code entre les différents éléments de l'interface. await déclarations. Bien sûr, vous l'avez comprimée en une seule ligne pour accomplir essentiellement cela, mais cela ne résout pas le problème. En outre, il est généralement mal vu, dans un contexte d'équipe, d'inclure plusieurs instructions sur une ligne de code donnée, car cela peut rendre le fichier source plus difficile à analyser par des yeux humains.

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X