Notes
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier des répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de modifier des répertoires.
Le modèle de programmation asynchrone de tâche (TAP) fournit une couche d’abstraction par rapport au codage asynchrone classique. Dans ce modèle, vous écrivez du code sous la forme d’une séquence d’instructions, comme d’habitude. La différence est que vous pouvez lire votre code basé sur des tâches en tant que compilateur traite chaque instruction et avant de commencer à traiter l’instruction suivante. Pour accomplir ce modèle, le compilateur effectue de nombreuses transformations pour effectuer chaque tâche. Certaines instructions peuvent lancer le travail et retourner un Task objet qui représente le travail en cours et le compilateur doit résoudre ces transformations. L’objectif de la programmation asynchrone de tâche est d’activer le code qui lit comme une séquence d’instructions, mais s’exécute dans un ordre plus compliqué. L’exécution est basée sur l’allocation de ressources externes et lorsque les tâches sont terminées.
Le modèle de programmation asynchrone de tâche est analogue à la façon dont les personnes donnent des instructions pour les processus qui incluent des tâches asynchrones. Cet article utilise un exemple avec des instructions pour préparer le petit déjeuner afin de montrer comment les mots clés async
et await
facilitent la compréhension du code qui inclut une série d’instructions asynchrones. Les instructions relatives à la création d’un petit-déjeuner peuvent être fournies sous forme de liste :
- Versez une tasse de café.
- Chauffer une poêle, puis faire frire deux œufs.
- Faites cuire trois galettes de pommes de terre.
- Faites griller deux tranches de pain.
- Étalez le beurre et la confiture sur le toast.
- Verser un verre de jus d’orange.
Si vous avez une expérience de cuisson, vous pouvez suivre ces instructions de manière asynchrone. Vous commencez à chauffer la poêle pour les œufs, puis vous commencez à cuire les pommes de terre rissolées. Vous mettez le pain dans le grille-pain, puis commencez à cuire les œufs. À chaque étape du processus, vous démarrez une tâche, puis passez à d’autres tâches prêtes pour votre attention.
La cuisson du petit déjeuner est un bon exemple de travail asynchrone qui n’est pas parallèle. Une personne (ou thread) peut gérer toutes les tâches. Une personne peut faire du petit déjeuner de manière asynchrone en démarrant la tâche suivante avant la fin de la tâche précédente. Chaque tâche de cuisson progresse, qu'une personne surveille activement le processus ou non. Dès que vous commencez à faire chauffer la poêle pour les œufs, vous pouvez commencer à cuire les pommes de terre rissolées. Une fois que les galettes de pommes de terre commencent à cuire, vous pouvez mettre le pain dans le grille-pain.
Pour un algorithme parallèle, vous avez besoin de plusieurs personnes qui cuisinent (ou plusieurs threads). Une personne fait cuire les œufs, une autre fait cuire les pommes de terre rissolées, et ainsi de suite. Chaque personne se concentre sur sa tâche spécifique. Chaque personne (ou chaque thread) qui effectue une préparation est bloquée de façon synchrone en attendant que la tâche en cours se termine : les pommes de terre rissolées prêtes à être retournées, le pain prêt à sauter dans le grille-pain, etc.
Considérez la même liste d’instructions synchrones écrites en tant qu’instructions de code C# :
using System;
using System.Threading.Tasks;
namespace AsyncBreakfast
{
// These classes are intentionally empty for the purpose of this example. They are simply marker classes for the purpose of demonstration, contain no properties, and serve no other purpose.
internal class HashBrown { }
internal class Coffee { }
internal class Egg { }
internal class Juice { }
internal class Toast { }
class Program
{
static void Main(string[] args)
{
Coffee cup = PourCoffee();
Console.WriteLine("coffee is ready");
Egg eggs = FryEggs(2);
Console.WriteLine("eggs are ready");
HashBrown hashBrown = FryHashBrowns(3);
Console.WriteLine("hash browns are ready");
Toast toast = ToastBread(2);
ApplyButter(toast);
ApplyJam(toast);
Console.WriteLine("toast is ready");
Juice oj = PourOJ();
Console.WriteLine("oj is ready");
Console.WriteLine("Breakfast is ready!");
}
private static Juice PourOJ()
{
Console.WriteLine("Pouring orange juice");
return new Juice();
}
private static void ApplyJam(Toast toast) =>
Console.WriteLine("Putting jam on the toast");
private static void ApplyButter(Toast toast) =>
Console.WriteLine("Putting butter on the toast");
private static Toast ToastBread(int slices)
{
for (int slice = 0; slice < slices; slice++)
{
Console.WriteLine("Putting a slice of bread in the toaster");
}
Console.WriteLine("Start toasting...");
Task.Delay(3000).Wait();
Console.WriteLine("Remove toast from toaster");
return new Toast();
}
private static HashBrown FryHashBrowns(int patties)
{
Console.WriteLine($"putting {patties} hash brown patties in the pan");
Console.WriteLine("cooking first side of hash browns...");
Task.Delay(3000).Wait();
for (int patty = 0; patty < patties; patty++)
{
Console.WriteLine("flipping a hash brown patty");
}
Console.WriteLine("cooking the second side of hash browns...");
Task.Delay(3000).Wait();
Console.WriteLine("Put hash browns on plate");
return new HashBrown();
}
private static Egg FryEggs(int howMany)
{
Console.WriteLine("Warming the egg pan...");
Task.Delay(3000).Wait();
Console.WriteLine($"cracking {howMany} eggs");
Console.WriteLine("cooking the eggs ...");
Task.Delay(3000).Wait();
Console.WriteLine("Put eggs on plate");
return new Egg();
}
private static Coffee PourCoffee()
{
Console.WriteLine("Pouring coffee");
return new Coffee();
}
}
}
Si vous interprétez ces instructions comme un ordinateur, le petit déjeuner prend environ 30 minutes pour se préparer. La durée correspond à la somme des heures de tâche individuelles. L’ordinateur bloque chaque instruction jusqu’à ce que tout le travail se termine, puis passe à l’instruction de tâche suivante. Cette approche peut prendre beaucoup de temps. Dans l’exemple de petit déjeuner, la méthode informatique crée un petit déjeuner insatisfaisant. Les tâches ultérieures de la liste synchrone, comme faire griller le pain, ne démarrent pas tant que les tâches précédentes ne sont pas terminées. Certains aliments sont froids avant que le petit déjeuner soit prêt à servir.
Si vous souhaitez que l’ordinateur exécute des instructions de manière asynchrone, vous devez écrire du code asynchrone. Lorsque vous écrivez des programmes clients, vous souhaitez que l’interface utilisateur soit réactive à l’entrée utilisateur. Votre application ne doit pas figer toutes les interactions lors du téléchargement de données à partir du web. Lorsque vous écrivez des programmes serveur, vous ne souhaitez pas bloquer les threads susceptibles de traiter d’autres demandes. L’utilisation de code synchrone lorsque des alternatives asynchrones existent nuit à votre capacité à effectuer un scale-out moins coûteux. Vous payez pour les threads bloqués.
Les applications modernes réussies nécessitent du code asynchrone. Sans prise en charge du langage, l’écriture de code asynchrone nécessite des rappels, des événements d’achèvement ou d’autres moyens qui masquent l’intention d’origine du code. L’avantage du code synchrone est l’action pas à pas qui facilite l’analyse et la compréhension. Les modèles asynchrones traditionnels vous obligent à vous concentrer sur la nature asynchrone du code, et non sur les actions fondamentales du code.
Ne bloquez pas, attendez à la place
Le code précédent met en évidence une pratique de programmation malheureuse : écriture de code synchrone pour effectuer des opérations asynchrones. Le code empêche le thread actuel d’effectuer tout autre travail. Le code n’interrompt pas le thread pendant l’exécution de tâches. Le résultat de ce modèle est similaire à regarder le grille-pain après y avoir mis le pain. Vous ignorez toute interruption et ne démarrez pas d’autres tâches tant que le pain n’est pas sorti. Vous ne prenez pas le beurre et la confiture hors du réfrigérateur. Vous pourriez ne pas remarquer un feu qui démarre sur la cuisinière. Vous voulez à la fois griller le pain et gérer d’autres préoccupations en même temps. Il en va de même avec votre code.
Vous pouvez commencer par mettre à jour le code afin que le thread ne bloque pas pendant l’exécution des tâches. Le await
mot clé fournit un moyen non bloquant de démarrer une tâche, puis de poursuivre l’exécution une fois la tâche terminée. Une version asynchrone simple du code de petit déjeuner ressemble à l’extrait de code suivant :
static async Task Main(string[] args)
{
Coffee cup = PourCoffee();
Console.WriteLine("coffee is ready");
Egg eggs = await FryEggsAsync(2);
Console.WriteLine("eggs are ready");
HashBrown hashBrown = await FryHashBrownsAsync(3);
Console.WriteLine("hash browns are ready");
Toast toast = await ToastBreadAsync(2);
ApplyButter(toast);
ApplyJam(toast);
Console.WriteLine("toast is ready");
Juice oj = PourOJ();
Console.WriteLine("oj is ready");
Console.WriteLine("Breakfast is ready!");
}
Le code met à jour les corps de méthode d’origine de FryEggs
, FryHashBrowns
et ToastBread
pour retourner Task<Egg>
, Task<HashBrown>
et Task<Toast>
les objets, respectivement. Les noms de méthode mis à jour incluent le suffixe « Async » : FryEggsAsync
, FryHashBrownsAsync
et ToastBreadAsync
. La Main
méthode retourne l’objet Task
, même s’il n’a pas d’expression return
, qui est par conception. Pour plus d’informations, consultez Évaluation d’une fonction asynchrone de retour void.
Remarque
Le code mis à jour ne tire pas encore parti des fonctionnalités clés de la programmation asynchrone, ce qui peut entraîner des temps d’achèvement plus courts. Le code traite les tâches dans environ la même durée que la version synchrone initiale. Pour connaître les implémentations complètes de méthode, consultez la version finale du code plus loin dans cet article.
Nous allons appliquer l’exemple de petit déjeuner au code mis à jour. Le thread ne bloque pas pendant la cuisson des œufs ou des pommes de terre rissolées, mais le code n'initie pas non plus d'autres tâches tant que le travail en cours n'est pas terminé. Vous mettez toujours le pain dans le grille-pain et vous regardez le grille-pain jusqu’à ce que le pain sorte, mais vous pouvez maintenant répondre aux interruptions. Dans un restaurant où plusieurs commandes sont passées, le cuisinier peut commencer une nouvelle commande alors qu’un autre est déjà en cuisine.
Dans le code mis à jour, le thread travaillant sur le petit-déjeuner n’est pas bloqué en attendant toute tâche démarrée qui n’est pas terminée. Pour certaines applications, cette modification est tout ce dont vous avez besoin. Vous pouvez permettre à votre application de prendre en charge l’interaction utilisateur pendant les téléchargements de données à partir du web. Dans d’autres scénarios, vous pouvez démarrer d’autres tâches en attendant que la tâche précédente se termine.
Démarrer des tâches simultanément
Pour la plupart des opérations, vous souhaitez démarrer immédiatement plusieurs tâches indépendantes. À mesure que chaque tâche se termine, vous lancez d’autres travaux prêts à commencer. Lorsque vous appliquez cette méthodologie à l’exemple de petit déjeuner, vous pouvez préparer le petit déjeuner plus rapidement. Vous préparez également tout en même temps, afin de pouvoir profiter d’un petit déjeuner chaud.
La System.Threading.Tasks.Task classe et les types associés sont des classes que vous pouvez utiliser pour appliquer ce style de raisonnement aux tâches en cours. Cette approche vous permet d’écrire du code qui ressemble plus étroitement à la façon dont vous créez le petit déjeuner en vie réelle. Vous commencez à cuire les œufs, les pommes de terre rissolées et les tartines grillées en même temps. Comme chaque élément alimentaire nécessite une action, vous faites attention à cette tâche, prenez soin de l’action, puis attendez quelque chose d’autre qui nécessite votre attention.
Dans votre code, vous démarrez une tâche et conservez l'objet Task qui représente le travail. Vous utilisez la await
méthode sur la tâche pour différer l'exécution jusqu'à ce que le résultat soit prêt.
Appliquez ces modifications au code du petit déjeuner. La première étape consiste à stocker les tâches pour les opérations au démarrage, plutôt que d’utiliser l’expression await
:
Coffee cup = PourCoffee();
Console.WriteLine("Coffee is ready");
Task<Egg> eggsTask = FryEggsAsync(2);
Egg eggs = await eggsTask;
Console.WriteLine("Eggs are ready");
Task<HashBrown> hashBrownTask = FryHashBrownsAsync(3);
HashBrown hashBrown = await hashBrownTask;
Console.WriteLine("Hash browns are ready");
Task<Toast> toastTask = ToastBreadAsync(2);
Toast toast = await toastTask;
ApplyButter(toast);
ApplyJam(toast);
Console.WriteLine("Toast is ready");
Juice oj = PourOJ();
Console.WriteLine("Oj is ready");
Console.WriteLine("Breakfast is ready!");
Ces révisions n’aident pas à préparer votre petit déjeuner plus rapidement. L’expression await
est appliquée à toutes les tâches dès qu’elles démarrent. L’étape suivante consiste à déplacer les expressions await
pour les pommes de terre rissolées et les œufs à la fin de la méthode, avant de servir le petit déjeuner :
Coffee cup = PourCoffee();
Console.WriteLine("Coffee is ready");
Task<Egg> eggsTask = FryEggsAsync(2);
Task<HashBrown> hashBrownTask = FryHashBrownsAsync(3);
Task<Toast> toastTask = ToastBreadAsync(2);
Toast toast = await toastTask;
ApplyButter(toast);
ApplyJam(toast);
Console.WriteLine("Toast is ready");
Juice oj = PourOJ();
Console.WriteLine("Oj is ready");
Egg eggs = await eggsTask;
Console.WriteLine("Eggs are ready");
HashBrown hashBrown = await hashBrownTask;
Console.WriteLine("Hash browns are ready");
Console.WriteLine("Breakfast is ready!");
Vous disposez maintenant d’un petit déjeuner préparé de manière asynchrone qui prend environ 20 minutes pour préparer. Le temps de cuisson total est réduit, car certaines tâches s’exécutent simultanément.
Le code améliore le processus de préparation en réduisant le temps de cuisson, mais il introduit une régression en brûlant les œufs et les pommes de terre rissolées. Vous démarrez toutes les tâches asynchrones à la fois. Vous n’attendez la fin d’une tâche que si vous avez besoin des résultats. Le code peut être similaire au programme dans une application web qui envoie des requêtes à différents microservices, puis combine les résultats dans une seule page. Vous effectuez toutes les requêtes immédiatement, puis appliquez l’expression await
sur toutes ces tâches et composez la page web.
Prise en charge de la composition des tâches
Les révisions de code précédentes permettent de préparer tout le petit déjeuner en même temps, à l’exception du toast. Le processus de fabrication du toast est une composition d’une opération asynchrone (toaster le pain) avec des opérations synchrones (répartir le beurre et la confiture sur le toast). Cet exemple illustre un concept important de programmation asynchrone :
Importante
La composition d’une opération asynchrone suivie d’un travail synchrone est une opération asynchrone. Indiqué d’une autre façon, si une partie d’une opération est asynchrone, l’opération entière est asynchrone.
Dans les mises à jour précédentes, vous avez appris à utiliser les objets Task ou Task<TResult> pour contenir des tâches en cours d'exécution. Vous attendez que chaque tâche soit terminée avant d'utiliser son résultat. L’étape suivante consiste à créer des méthodes qui représentent la combinaison d’autres travaux. Avant de servir le petit déjeuner, vous voulez attendre que la tâche de griller le pain soit terminée avant d'étaler le beurre et la confiture.
Vous pouvez représenter ce travail avec le code suivant :
static async Task<Toast> MakeToastWithButterAndJamAsync(int number)
{
var toast = await ToastBreadAsync(number);
ApplyButter(toast);
ApplyJam(toast);
return toast;
}
La MakeToastWithButterAndJamAsync
méthode a le async
modificateur dans sa signature qui signale au compilateur que la méthode contient une await
expression et contient des opérations asynchrones. La méthode représente la tâche qui grille le pain, puis étale le beurre et la confiture. La méthode retourne un Task<TResult> objet qui représente la composition des trois opérations.
Le bloc principal révisé de code ressemble maintenant à ceci :
static async Task Main(string[] args)
{
Coffee cup = PourCoffee();
Console.WriteLine("coffee is ready");
var eggsTask = FryEggsAsync(2);
var hashBrownTask = FryHashBrownsAsync(3);
var toastTask = MakeToastWithButterAndJamAsync(2);
var eggs = await eggsTask;
Console.WriteLine("eggs are ready");
var hashBrown = await hashBrownTask;
Console.WriteLine("hash browns are ready");
var toast = await toastTask;
Console.WriteLine("toast is ready");
Juice oj = PourOJ();
Console.WriteLine("oj is ready");
Console.WriteLine("Breakfast is ready!");
}
Cette modification de code illustre une technique importante pour l’utilisation du code asynchrone. Vous composez des tâches en séparant les opérations en une nouvelle méthode qui retourne une tâche. Vous pouvez choisir dans quels cas attendre les tâches. Vous pouvez démarrer d’autres tâches simultanément.
Gérer les exceptions asynchrones
Jusqu’à ce stade, votre code suppose implicitement que toutes les tâches se terminent correctement. Les méthodes asynchrones lèvent des exceptions, tout comme leurs équivalents synchrones. Les objectifs de prise en charge asynchrone des exceptions et de la gestion des erreurs sont les mêmes que pour la prise en charge asynchrone en général. La meilleure pratique consiste à écrire du code qui lit comme une série d’instructions synchrones. Les tâches génèrent des exceptions lorsqu'elles ne peuvent pas se terminer correctement. Le code client peut intercepter ces exceptions lorsque l’expression await
est appliquée à une tâche démarrée.
Dans l’exemple de petit déjeuner, supposons que le grille-pain attrape le feu tout en grillant le pain. Vous pouvez simuler ce problème en modifiant la ToastBreadAsync
méthode pour qu’elle corresponde au code suivant :
private static async Task<Toast> ToastBreadAsync(int slices)
{
for (int slice = 0; slice < slices; slice++)
{
Console.WriteLine("Putting a slice of bread in the toaster");
}
Console.WriteLine("Start toasting...");
await Task.Delay(2000);
Console.WriteLine("Fire! Toast is ruined!");
throw new InvalidOperationException("The toaster is on fire");
await Task.Delay(1000);
Console.WriteLine("Remove toast from toaster");
return new Toast();
}
Remarque
Lorsque vous compilez ce code, vous voyez un avertissement sur le code inaccessible. Cette erreur est normale. Après que le grille-pain prend feu, les opérations ne se poursuivent pas normalement et le code retourne une erreur.
Après avoir apporté les modifications du code, exécutez l’application et vérifiez la sortie :
Pouring coffee
Coffee is ready
Warming the egg pan...
putting 3 hash brown patties in the pan
Cooking first side of hash browns...
Putting a slice of bread in the toaster
Putting a slice of bread in the toaster
Start toasting...
Fire! Toast is ruined!
Flipping a hash brown patty
Flipping a hash brown patty
Flipping a hash brown patty
Cooking the second side of hash browns...
Cracking 2 eggs
Cooking the eggs ...
Put hash browns on plate
Put eggs on plate
Eggs are ready
Hash browns are ready
Unhandled exception. System.InvalidOperationException: The toaster is on fire
at AsyncBreakfast.Program.ToastBreadAsync(Int32 slices) in Program.cs:line 65
at AsyncBreakfast.Program.MakeToastWithButterAndJamAsync(Int32 number) in Program.cs:line 36
at AsyncBreakfast.Program.Main(String[] args) in Program.cs:line 24
at AsyncBreakfast.Program.<Main>(String[] args)
Notez que pas mal de tâches se terminent entre le moment où le grille-pain prend feu et celui où le système observe l’exception. Lorsqu’une tâche qui s’exécute de façon asynchrone lève une exception, cette tâche est défectueuse. L’objet Task
contient l’exception générée par la propriété Task.Exception. Les tâches défectueuses lèvent une exception lorsque l’expression await
est appliquée à la tâche.
Il existe deux mécanismes importants pour comprendre ce processus :
- Comment une exception est stockée dans une tâche défectueuse
- Comment une exception est désassemblée et à nouveau levée quand du code attend (
await
) après une tâche défaillante
Lorsque le code en cours d’exécution lève de façon asynchrone une exception, l’exception est stockée dans l’objet Task
. La propriété Task.Exception est un objet System.AggregateException car plusieurs exceptions peuvent être levées pendant le travail asynchrone. Toute exception levée est ajoutée à la collection AggregateException.InnerExceptions. Si la propriété Exception
est nulle, un AggregateException
est créé et l'exception générée est le premier élément de la collection.
Le scénario le plus courant pour une tâche défectueuse est que la Exception
propriété contient exactement une exception. Quand votre code attend après une tâche défaillante, il lève à nouveau la première exception AggregateException.InnerExceptions de la collection. Ce résultat est la raison pour laquelle la sortie de l’exemple montre un System.InvalidOperationException objet plutôt qu’un AggregateException
objet. L’extraction de la première exception interne permet d’utiliser des méthodes asynchrones aussi similaires que possible pour utiliser leurs équivalents synchrones. Vous pouvez examiner la Exception
propriété dans votre code lorsque votre scénario peut générer plusieurs exceptions.
Conseil / Astuce
La pratique recommandée consiste à ce que toutes les exceptions de validation des arguments soient signalées de manière synchrone par les méthodes renvoyant des tâches. Pour plus d’informations et d’exemples, consultez Exceptions dans les méthodes de retour de tâches.
Avant de passer à la section suivante, commentez les deux instructions suivantes dans votre méthode ToastBreadAsync
. Vous ne voulez pas déclencher un autre feu :
Console.WriteLine("Fire! Toast is ruined!");
throw new InvalidOperationException("The toaster is on fire");
Appliquez efficacement des expressions await aux tâches
Vous pouvez améliorer la série d’expressions await
à la fin du code précédent à l’aide de méthodes de la Task
classe. Une API est la WhenAll méthode, qui retourne un Task objet qui se termine lorsque toutes les tâches de sa liste d’arguments sont terminées. Le code suivant illustre cette méthode :
await Task.WhenAll(eggsTask, hashBrownTask, toastTask);
Console.WriteLine("Eggs are ready");
Console.WriteLine("Hash browns are ready");
Console.WriteLine("Toast is ready");
Console.WriteLine("Breakfast is ready!");
Une autre option consiste à utiliser la WhenAny méthode, qui retourne un Task<Task>
objet qui se termine lorsque l’un de ses arguments est terminé. Vous pouvez attendre la tâche retournée, car vous savez que la tâche est terminée. Le code suivant montre comment utiliser la WhenAny méthode pour attendre la fin de la première tâche, puis traiter son résultat. Une fois que vous avez traité le résultat de la tâche terminée, vous supprimez la tâche terminée de la liste des tâches passées à la WhenAny
méthode.
var breakfastTasks = new List<Task> { eggsTask, hashBrownTask, toastTask };
while (breakfastTasks.Count > 0)
{
Task finishedTask = await Task.WhenAny(breakfastTasks);
if (finishedTask == eggsTask)
{
Console.WriteLine("Eggs are ready");
}
else if (finishedTask == hashBrownTask)
{
Console.WriteLine("Hash browns are ready");
}
else if (finishedTask == toastTask)
{
Console.WriteLine("Toast is ready");
}
await finishedTask;
breakfastTasks.Remove(finishedTask);
}
Près de la fin de l’extrait de code, notez l’expression await finishedTask;
. L’expression await Task.WhenAny
n’attend pas la tâche terminée, mais attend plutôt l’objet Task
retourné par la Task.WhenAny
méthode. Le résultat de la Task.WhenAny
méthode est la tâche terminée (ou défectueuse). La meilleure pratique consiste à attendre à nouveau la tâche, même lorsque vous savez que la tâche est terminée. De cette manière, vous pouvez récupérer le résultat de la tâche ou vérifier que toute exception qui provoque l’erreur de la tâche est levée.
Passer en revue le code final
Voici à quoi ressemble la version finale du code :
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace AsyncBreakfast
{
// These classes are intentionally empty for the purpose of this example. They are simply marker classes for the purpose of demonstration, contain no properties, and serve no other purpose.
internal class HashBrown { }
internal class Coffee { }
internal class Egg { }
internal class Juice { }
internal class Toast { }
class Program
{
static async Task Main(string[] args)
{
Coffee cup = PourCoffee();
Console.WriteLine("coffee is ready");
var eggsTask = FryEggsAsync(2);
var hashBrownTask = FryHashBrownsAsync(3);
var toastTask = MakeToastWithButterAndJamAsync(2);
var breakfastTasks = new List<Task> { eggsTask, hashBrownTask, toastTask };
while (breakfastTasks.Count > 0)
{
Task finishedTask = await Task.WhenAny(breakfastTasks);
if (finishedTask == eggsTask)
{
Console.WriteLine("eggs are ready");
}
else if (finishedTask == hashBrownTask)
{
Console.WriteLine("hash browns are ready");
}
else if (finishedTask == toastTask)
{
Console.WriteLine("toast is ready");
}
await finishedTask;
breakfastTasks.Remove(finishedTask);
}
Juice oj = PourOJ();
Console.WriteLine("oj is ready");
Console.WriteLine("Breakfast is ready!");
}
static async Task<Toast> MakeToastWithButterAndJamAsync(int number)
{
var toast = await ToastBreadAsync(number);
ApplyButter(toast);
ApplyJam(toast);
return toast;
}
private static Juice PourOJ()
{
Console.WriteLine("Pouring orange juice");
return new Juice();
}
private static void ApplyJam(Toast toast) =>
Console.WriteLine("Putting jam on the toast");
private static void ApplyButter(Toast toast) =>
Console.WriteLine("Putting butter on the toast");
private static async Task<Toast> ToastBreadAsync(int slices)
{
for (int slice = 0; slice < slices; slice++)
{
Console.WriteLine("Putting a slice of bread in the toaster");
}
Console.WriteLine("Start toasting...");
await Task.Delay(3000);
Console.WriteLine("Remove toast from toaster");
return new Toast();
}
private static async Task<HashBrown> FryHashBrownsAsync(int patties)
{
Console.WriteLine($"putting {patties} hash brown patties in the pan");
Console.WriteLine("cooking first side of hash browns...");
await Task.Delay(3000);
for (int patty = 0; patty < patties; patty++)
{
Console.WriteLine("flipping a hash brown patty");
}
Console.WriteLine("cooking the second side of hash browns...");
await Task.Delay(3000);
Console.WriteLine("Put hash browns on plate");
return new HashBrown();
}
private static async Task<Egg> FryEggsAsync(int howMany)
{
Console.WriteLine("Warming the egg pan...");
await Task.Delay(3000);
Console.WriteLine($"cracking {howMany} eggs");
Console.WriteLine("cooking the eggs ...");
await Task.Delay(3000);
Console.WriteLine("Put eggs on plate");
return new Egg();
}
private static Coffee PourCoffee()
{
Console.WriteLine("Pouring coffee");
return new Coffee();
}
}
}
Le code effectue les tâches de petit déjeuner asynchrones en environ 15 minutes. Le temps total est réduit, car certaines tâches s’exécutent simultanément. Le code surveille simultanément plusieurs tâches et prend des mesures uniquement si nécessaire.
Le code final est asynchrone. Il reflète plus précisément la façon dont une personne peut cuisiner le petit déjeuner. Comparez le code final avec le premier exemple de code de l’article. Les actions principales sont toujours claires en lisant le code. Vous pouvez lire le code final de la même façon que vous lisez la liste des instructions pour faire un petit déjeuner, comme indiqué au début de l’article. Les fonctionnalités linguistiques des mots-clés async
et await
aident chaque personne à traduire les instructions écrites : Commencez les tâches dès que possible et ne restez pas bloqué en attendant qu'elles se terminent.
Comparaison entre Async/await et ContinueWith
Les mots clés async
et await
fournissent une simplification syntaxique par rapport à l'utilisation directe de Task.ContinueWith. Bien que async
/await
et ContinueWith
aient une sémantique similaire pour la gestion des opérations asynchrones, le compilateur ne traduit pas nécessairement directement les expressions await
en appels de méthode ContinueWith
. Au lieu de cela, le compilateur génère du code d’ordinateur d’état optimisé qui fournit le même comportement logique. Cette transformation offre des avantages significatifs en matière de lisibilité et de maintenance, en particulier lors du chaînage de plusieurs opérations asynchrones.
Envisagez un scénario dans lequel vous devez effectuer plusieurs opérations asynchrones séquentielles. Voici comment la même logique s'applique quand elle est implémentée avec ContinueWith
par rapport à async
/await
:
Utilisation de ContinueWith
Avec ContinueWith
, chaque étape d’une séquence d’opérations asynchrones nécessite des continuations imbriquées :
// Using ContinueWith - demonstrates the complexity when chaining operations
static Task MakeBreakfastWithContinueWith()
{
return StartCookingEggsAsync()
.ContinueWith(eggsTask =>
{
var eggs = eggsTask.Result;
Console.WriteLine("Eggs ready, starting bacon...");
return StartCookingBaconAsync();
})
.Unwrap()
.ContinueWith(baconTask =>
{
var bacon = baconTask.Result;
Console.WriteLine("Bacon ready, starting toast...");
return StartToastingBreadAsync();
})
.Unwrap()
.ContinueWith(toastTask =>
{
var toast = toastTask.Result;
Console.WriteLine("Toast ready, applying butter...");
return ApplyButterAsync(toast);
})
.Unwrap()
.ContinueWith(butteredToastTask =>
{
var butteredToast = butteredToastTask.Result;
Console.WriteLine("Butter applied, applying jam...");
return ApplyJamAsync(butteredToast);
})
.Unwrap()
.ContinueWith(finalToastTask =>
{
var finalToast = finalToastTask.Result;
Console.WriteLine("Breakfast completed with ContinueWith!");
});
}
Utilisation d’async-await
La même séquence d’opérations avec async
/await
est beaucoup plus naturelle.
// Using async/await - much cleaner and easier to read
static async Task MakeBreakfastWithAsyncAwait()
{
var eggs = await StartCookingEggsAsync();
Console.WriteLine("Eggs ready, starting bacon...");
var bacon = await StartCookingBaconAsync();
Console.WriteLine("Bacon ready, starting toast...");
var toast = await StartToastingBreadAsync();
Console.WriteLine("Toast ready, applying butter...");
var butteredToast = await ApplyButterAsync(toast);
Console.WriteLine("Butter applied, applying jam...");
var finalToast = await ApplyJamAsync(butteredToast);
Console.WriteLine("Breakfast completed with async/await!");
}
Pourquoi async/await est préféré
L’approche async
/await
offre plusieurs avantages :
- Lisibilité : le code lit comme du code synchrone, ce qui facilite la compréhension du flux d’opérations.
- Facilité de maintenance : l’ajout ou la suppression d’étapes dans la séquence nécessite des modifications de code minimales.
- Gestion des erreurs : La gestion des exceptions avec
try
/catch
des blocs fonctionne naturellement, tandis que nécessiteContinueWith
une gestion minutieuse des tâches défectueuses. - Débogage : avec
async
/await
, la pile d'appels et l'expérience du débogueur sont grandement améliorées. - Performances : les optimisations du compilateur sont
async
/await
plus sophistiquées que les chaînes manuelles.ContinueWith
L’avantage devient encore plus évident à mesure que le nombre d’opérations chaînées augmente. Bien qu’une continuation unique puisse être gérable avec ContinueWith
, les séquences de 3 à 4 opérations asynchrones deviennent rapidement difficiles à lire et à gérer. Ce modèle, appelé « do-notation monadicique » dans la programmation fonctionnelle, vous permet de composer plusieurs opérations asynchrones de manière séquentielle et lisible.