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();
}
}
}
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.
Je crois comprendre que la principale raison de préférer Task.WhenAll
à plusieurs await
s est la performance/tâche "barattage": le DoWork1
La méthode fait quelque chose comme ça:
Par contre, DoWork2
est ce que ca:
Que cela soit assez important pour votre cas particulier dépend bien sûr du contexte (dépendez du jeu de mots).
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);
}
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é.
(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)
{
// ...
}
}