web-dev-qa-db-fra.com

Pourquoi devrais-je préférer "attendre la tâche. Quand tout" sur plusieurs attend?

Si je ne me soucie pas de l'ordre d'achèvement de la tâche et que j'ai juste besoin qu'ils soient tous terminés, dois-je quand même utiliser await Task.WhenAll _ au lieu de plusieurs await? par exemple, est DoWork2 ci-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();
        }
    }
}
101
avo

Oui, utilisez WhenAll car il propage toutes les erreurs en même temps. Si vous attendez plusieurs fois, vous perdez des erreurs si l’un des plus tôt attend.

Une autre différence importante est que WhenAll attendra que toutes les tâches soient terminées même en cas d'échec (tâches défaillantes ou annulées). Attendre manuellement en séquence provoquerait une concurrence inattendue car la partie de votre programme qui souhaite attendre continuera en fait 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.

94
usr

Je crois comprendre que la principale raison de préférer Task.WhenAll à plusieurs awaits est la performance/tâche "barattage": le DoWork1 La méthode fait quelque chose comme ça:

  • commencer par un contexte
  • sauver le contexte
  • attendez t1
  • restaurer le contexte d'origine
  • sauver le contexte
  • attendez t2
  • restaurer le contexte d'origine
  • sauver le contexte
  • attendez t3
  • restaurer le contexte d'origine

Par contre, DoWork2 est ce que ca:

  • commencer par un contexte donné
  • sauver le contexte
  • attendre tous les t1, t2 et t3
  • restaurer le contexte d'origine

Que cela soit assez important pour votre cas particulier dépend bien sûr du contexte (dépendez du jeu de mots).

22
Marcel Popescu

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

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

Lorsque vous utilisez Task.WhenAll il est possible de conserver ce code de procédure accélérée tout en veillant à ce que l'appelant puisse 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);
}
14
Lukazoid

Les autres réponses à cette question offrent des raisons techniques pour lesquelles on préfère await Task.WhenAll(t1, t2, t3);. Cette réponse visera à la regarder d'un côté plus doux (auquel @usr fait allusion) tout en tirant 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 (ni même votre futur moi!) D'ajouter du code entre les différentes instructions await. Bien sûr, vous avez compressé le texte sur une ligne pour essentiellement y parvenir, mais cela ne résout pas le problème. En outre, dans un environnement d’équipe, il est généralement mauvais d’inclure plusieurs instructions sur une ligne de code donnée, car cela peut rendre le fichier source plus difficile à analyser.

Autrement dit, await Task.WhenAll(t1, t2, t3); est plus facile à gérer, car il communique votre intention plus clairement et est moins vulnérable aux bogues particuliers pouvant provenir de mises à jour bien intentionnées du code, ou même simplement d'une fusion ayant mal fonctionné.

4
rarrarrarrr

(Avertissement: Cette réponse est inspirée du cours TPL Async de 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 supposez qu'ils 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 jettent des exceptions, seule la première sera interceptée. Toute exception ultérieure sera perdue. C'est à dire. si t2 et t3 lèvent une exception, seul t2 sera capturé; etc. Les exceptions de tâches suivantes resteront inobservé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é wait renvoie toujours toujours la première exception. Les autres exceptions ne sont donc toujours pas observées. Une façon de résoudre 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é result générera 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)
    {
        // ...
    }
}
3
David Refaeli