Modèle de programmation asynchrone des tâches en C#The Task asynchronous programming model in C#

Le modèle de programmation asynchrone des tâches fournit une abstraction pour le code asynchrone.The Task asynchronous programming model (TAP) provides an abstraction over asynchronous code. Pour cela, vous écrivez votre code comme d’habitude, sous la forme d’une suite d’instructions.You write code as a sequence of statements, just like always. Vous pouvez lire ce code comme si chaque instruction se terminait avant que la suivante ne commence.You can read that code as though each statement completes before the next begins. Le compilateur effectue un certain nombre de transformations, car certaines de ces instructions peuvent commencer le travail et retourner un Task qui représente le travail en cours.The compiler performs a number of transformations because some of those statements may start work and return a Task that represents the ongoing work.

C’est en fait l’objectif de cette syntaxe : permettre au code d’être lu comme une suite d’instructions, mais d’être exécuté dans un ordre beaucoup plus complexe, en fonction de l’allocation des ressources externes et du moment auquel se terminent les tâches.That's the goal of this syntax: enable code that reads like a sequence of statements, but executes in a much more complicated order based on external resource allocation and when tasks complete. Cela revient à donner des instructions pour des processus qui comprennent des tâches asynchrones.It's analogous to how people give instructions for processes that include asynchronous tasks. Dans cet article, vous allez utiliser un exemple d’instructions pour la préparation d’un petit-déjeuner. Vous verrez comment les mots clés async et await facilitent la compréhension du code dans lequel se trouve une série d’instructions asynchrones.Throughout this article, you'll use an example of instructions for making a breakfast to see how the async and await keywords make it easier to reason about code that includes a series of asynchronous instructions. Pour expliquer comment préparer un petit-déjeuner, vous pourriez écrire des instructions telles que les suivantes :You'd write the instructions something like the following list to explain how to make a breakfast:

  1. Verser le café dans une tasse.Pour a cup of coffee.
  2. Faire chauffer la poêle, puis faire cuire deux œufs au plat.Heat up a pan, then fry two eggs.
  3. Faire frire trois tranches de bacon.Fry three slices of bacon.
  4. Faire toaster deux tranches de pain.Toast two pieces of bread.
  5. Étaler le beurre et la confiture sur les toasts.Add butter and jam to the toast.
  6. Verser du jus d’orange dans un verre.Pour a glass of orange juice.

Si vous avez l’habitude de cuisiner, vous savez que ces instructions doivent être exécutées de manière asynchrone.If you have experience with cooking, you'd execute those instructions asynchronously. En effet, vous commencez par faire chauffer la poêle pour les œufs, mais vous faites d’abord frire le bacon.You'd start warming the pan for eggs, then start the bacon. Ensuite, vous mettez les tranches de pain dans le grille-pain, puis vous cassez les œufs dans la poêle.You'd put the bread in the toaster, then start the eggs. À chaque étape du processus, vous démarrez une tâche, puis déplacez votre attention vers les tâches qui sont prêtes à être réalisées.At each step of the process, you'd start a task, then turn your attention to tasks that are ready for your attention.

La préparation du petit-déjeuner est un bon exemple de travail asynchrone mais non parallèle.Cooking breakfast is a good example of asynchronous work that isn't parallel. Une personne (ou un thread) peut gérer toutes ces tâches.One person (or thread) can handle all these tasks. Pour poursuivre l’analogie avec le petit-déjeuner, une personne peut préparer le petit-déjeuner de façon asynchrone en démarrant une tâche avant que la précédente ne soit terminée.Continuing the breakfast analogy, one person can make breakfast asynchronously by starting the next task before the first completes. En outre, la cuisson se poursuit, que vous la surveilliez ou non.The cooking progresses whether or not someone is watching it. Dès que vous commencez à faire chauffer la poêle pour les œufs, vous pouvez mettre le bacon à frire.As soon as you start warming the pan for the eggs, you can begin frying the bacon. Quand le bacon est en train de frire, vous pouvez mettre les tranches de pain dans le grille-pain.Once the bacon starts, you can put the bread into the toaster.

Pour un algorithme parallèle, vous auriez besoin de plusieurs cuisiniers (ou threads).For a parallel algorithm, you'd need multiple cooks (or threads). L’un d’eux s’occuperait des œufs, un autre du bacon, etc.One would make the eggs, one the bacon, and so on. Chacun d’eux serait concentré sur une seule tâche.Each one would be focused on just that one task. Chaque cuisinier (ou thread) serait bloqué de façon synchrone, car il devrait attendre que le bacon soit prêt à être retourné ou que le pain soit grillé.Each cook (or thread) would be blocked synchronously waiting for bacon to be ready to flip, or the toast to pop.

Écrivons maintenant ces instructions sous la forme d’instructions C# :Now, consider those same instructions written as C# statements:

static void Main(string[] args)
{
    Coffee cup = PourCoffee();
    Console.WriteLine("coffee is ready");
    Egg eggs = FryEggs(2);
    Console.WriteLine("eggs are ready");
    Bacon bacon = FryBacon(3);
    Console.WriteLine("bacon is 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!");
}

Les ordinateurs n’interprètent pas ces instructions de la même façon que les humains.Computers don't interpret those instructions the same way people do. L’ordinateur se bloque à chaque instruction jusqu’à ce que le travail soit terminé, avant de passer à l’instruction suivante.The computer will block on each statement until the work is complete before moving on to the next statement. Avec une telle manière de procéder, notre petit-déjeuner risque de ne pas être très satisfaisant.That creates an unsatisfying breakfast. En effet, chacune des tâches ne pourrait être démarrée qu’une fois la précédente terminée.The later tasks wouldn't be started until the earlier tasks had completed. De cette façon, la préparation du petit-déjeuner prendrait beaucoup plus de temps, et certains aliments refroidiraient avant d’être servis.It would take much longer to create the breakfast, and some items would have gotten cold before being served.

Si vous souhaitez que l’ordinateur exécute les instructions ci-dessus de façon asynchrone, vous devez écrire du code asynchrone.If you want the computer to execute the above instructions asynchronously, you must write asynchronous code.

De nos jours, ces remarques sont très importantes pour l’écriture de programmes.These concerns are important for the programs you write today. Lorsque vous écrivez des programmes clients, l’interface utilisateur doit être réactive aux entrées utilisateur.When you write client programs, you want the UI to be responsive to user input. Votre application ne doit pas donner l’impression que votre smartphone se bloque pour télécharger des données à partir du web.Your application shouldn't make a phone appear frozen while it's downloading data from the web. Lorsque vous écrivez des programmes de serveur, vous ne devez pas bloquer les threads.When you write server programs, you don't want threads blocked. En effet, ces threads peuvent servir à d’autres requêtes.Those threads could be serving other requests. Lorsque d’autres alternatives sont possibles, il est dommage d’utiliser du code synchrone, car celui-ci ne vous permet pas de réduire les frais des scale-out.Using synchronous code when asynchronous alternatives exist hurts your ability to scale out less expensively. Les threads bloqués ont un prix.You pay for those blocked threads.

Une application moderne efficace a besoin de code asynchrone.Successful modern applications require asynchronous code. Avant la prise en charge du langage, l’écriture de code asynchrone nécessitait des rappels, des événements de complétion ou d’autres méthodes qui masquaient l’objectif premier du code.Without language support, writing asynchronous code required callbacks, completion events, or other means that obscured the original intent of the code. L’avantage du code synchrone est qu’il est facile à comprendre.The advantage of the synchronous code is that it's easy to understand. Les actions détaillées étape par étape facilitent son parcours et sa compréhension.The step-by-step actions make it easy to scan and understand. Les modèles asynchrones traditionnels vous obligeaient à vous concentrer sur la nature asynchrone du code, et non sur ses actions fondamentales.Traditional asynchronous models forced you to focus on the asynchronous nature of the code, not on the fundamental actions of the code.

Éviter les blocages avec AwaitDon't block, await instead

Le code précédent montre une pratique déconseillée : écrire du code synchrone pour effectuer des opérations asynchrones.The preceding code demonstrates a bad practice: constructing synchronous code to perform asynchronous operations. Comme nous l’avons vu, ce code bloque le thread, qui ne peut pas exécuter d’autres tâches.As written, this code blocks the thread executing it from doing any other work. Il ne sera pas interrompu tant que l’une des tâches est en cours d’exécution.It won't be interrupted while any of the tasks are in progress. Ce serait comme rester les yeux fixés sur le grille-pain après avoir mis le pain dedans.It would be as though you stared at the toaster after putting the bread in. Vous n’écouteriez personne tant que le pain ne serait pas ressorti du grille-pain.You'd ignore anyone talking to you until the toast popped.

Commençons par mettre à jour ce code pour que le thread ne se bloque pas pendant l’exécution des tâches.Let's start by updating this code so that the thread doesn't block while tasks are running. Le mot clé await permet de démarrer une tâche sans bloquer le thread, puis de poursuivre l’exécution une fois cette tâche terminée.The await keyword provides a non-blocking way to start a task, then continue execution when that task completes. La version asynchrone du code de préparation du petit-déjeuner ressemblerait à l’extrait de code suivant :A simple asynchronous version of the make a breakfast code would look like the following snippet:

static async Task Main(string[] args)
{
    Coffee cup = PourCoffee();
    Console.WriteLine("coffee is ready");
    Egg eggs = await FryEggs(2);
    Console.WriteLine("eggs are ready");
    Bacon bacon = await FryBacon(3);
    Console.WriteLine("bacon is ready");
    Toast toast = await ToastBread(2);
    ApplyButter(toast);
    ApplyJam(toast);
    Console.WriteLine("toast is ready");
    Juice oj = PourOJ();
    Console.WriteLine("oj is ready");

    Console.WriteLine("Breakfast is ready!");
}

Ce code ne se bloque pas pendant la cuisson des œufs ou du bacon.This code doesn't block while the eggs or the bacon are cooking. Cependant, il ne démarre pas d’autres tâches.This code won't start any other tasks though. Cela reviendrait, une nouvelle fois, à rester les yeux fixés sur le grille-pain en attendant que le pain ressorte.You'd still put the toast in the toaster and stare at it until it pops. Mais au moins, vous seriez en mesure de répondre à quelqu’un qui aurait besoin de votre attention.But at least, you'd respond to anyone that wanted your attention. Dans un restaurant qui doit gérer plusieurs commandes en même temps, le chef peut commencer un petit-déjeuner pendant qu’un autre est en cours de préparation.In a restaurant where multiple orders are placed, the cook could start another breakfast while the first is cooking.

Ici, le thread qui prépare le petit-déjeuner n’est pas bloqué en attendant qu’une tâche démarrée se termine.Now, the thread working on the breakfast isn't blocked while awaiting any started task that hasn't yet finished. Pour certaines applications, cette modification est la seule nécessaire.For some applications, this change is all that's needed. Ce simple changement permet à une application GUI de continuer à répondre à l’utilisateur.A GUI application still responds to the user with just this change. Toutefois, pour ce scénario, d’autres modifications sont nécessaires.However, for this scenario, you want more. Il n’est pas souhaitable que chaque tâche de composant soit exécutée de manière séquentielle.You don't want each of the component tasks to be executed sequentially. Il est préférable de démarrer chacune des tâches de composant avant d’attendre l’achèvement de la tâche précédente.It's better to start each of the component tasks before awaiting the previous task's completion.

Démarrer plusieurs tâches simultanémentStart tasks concurrently

Dans de nombreux scénarios, il est nécessaire de démarrer plusieurs tâches immédiatement.In many scenarios, you want to start several independent tasks immediately. Ensuite, à la fin de chaque tâche, vous pouvez passer aux autres tâches qui sont prêtes.Then, as each task finishes, you can continue other work that's ready. C’est de cette façon que nous pouvons accélérer la préparation de notre petit-déjeuner.In the breakfast analogy, that's how you get breakfast done more quickly. Cela permet également de tout terminer en même temps, ou presque.You also get everything done close to the same time. Cette façon de procéder vous garantit que vous mangerez votre petit-déjeuner bien chaud.You'll get a hot breakfast.

System.Threading.Tasks.Task et les types associés sont des classes que vous pouvez utiliser pour organiser les tâches en cours.The System.Threading.Tasks.Task and related types are classes you can use to reason about tasks that are in progress. Cela vous permet d’écrire du code qui se rapproche davantage de la façon dont vous devez préparer un petit-déjeuner,That enables you to write code that more closely resembles the way you'd actually create breakfast. puisque vous commencez à faire cuire les œufs, à faire frire le bacon et à faire griller le pain simultanément.You'd start cooking the eggs, bacon, and toast at the same time. Puisque toutes ces tâches nécessitent une action, vous devez porter votre attention sur une tâche, démarrer l’action suivante, puis attendre qu’une autre tâche nécessite votre attention.As each requires action, you'd turn your attention to that task, take care of the next action, then await for something else that requires your attention.

Vous démarrez une tâche puis vous gardez l’objet Task qui représente le travail.You start a task and hold on to the Task object that represents the work. Vous devez attendre (await) chaque tâche avant d’utiliser ses résultats.You'll await each task before working with its result.

À présent, apportons ces modifications au code de notre petit-déjeuner.Let's make these changes to the breakfast code. La première étape consiste à stocker les tâches des opérations dès leur démarrage, plutôt que d’attendre la fin de leur exécution :The first step is to store the tasks for operations when they start, rather than awaiting them:

Coffee cup = PourCoffee();
Console.WriteLine("coffee is ready");
Task<Egg> eggTask = FryEggs(2);
Egg eggs = await eggTask;
Console.WriteLine("eggs are ready");
Task<Bacon> baconTask = FryBacon(3);
Bacon bacon = await baconTask;
Console.WriteLine("bacon is ready");
Task<Toast> toastTask = ToastBread(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!");

Ensuite, vous pouvez déplacer les instructions await pour le bacon et les œufs à la fin de la méthode, avant de servir le petit-déjeuner :Next, you can move the await statements for the bacon and eggs to the end of the method, before serving breakfast:

Coffee cup = PourCoffee();
Console.WriteLine("coffee is ready");
Task<Egg> eggTask = FryEggs(2);
Task<Bacon> baconTask = FryBacon(3);
Task<Toast> toastTask = ToastBread(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 eggTask;
Console.WriteLine("eggs are ready");
Bacon bacon = await baconTask;
Console.WriteLine("bacon is ready");

Console.WriteLine("Breakfast is ready!");

Le code précédent est plus efficace.The preceding code works better. Vous y démarrez toutes les tâches asynchrones en même temps.You start all the asynchronous tasks at once. Vous n’attendez la fin d’une tâche que si vous avez besoin des résultats.You await each task only when you need the results. Le code précédent est semblable au code des applications web qui envoient des requêtes à différents microservices, puis qui combinent les résultats au sein d’une même page.The preceding code may be similar to code in a web application that makes requests of different microservices, then combines the results into a single page. Vous allez exécuter toutes les requêtes en même temps, puis vous allez attendre (await) toutes ces tâches pour composer la page web.You'll make all the requests immediately, then await all those tasks and compose the web page.

Composition de tâchesComposition with tasks

Tout est prêt en même temps pour votre petit-déjeuner, sauf les toasts.You have everything ready for breakfast at the same time except the toast. La préparation des toasts correspond à la composition d’une opération asynchrone (faire griller le pain) et d’opérations synchrones (ajouter du beurre et de la confiture).Making the toast is the composition of an asynchronous operation (toasting the bread), and synchronous operations (adding the butter and the jam). Le code ci-dessous illustre un concept important :Updating this code illustrates an important concept:

Important

La composition d’une opération asynchrone et d’une tâche synchrone constitue une opération asynchrone.The composition of an asynchronous operation followed by synchronous work is an asynchronous operation. Autrement dit, si une partie d’une opération est asynchrone, cela rend asynchrone l’intégralité de l’opération.Stated another way, if any portion of an operation is asynchronous, the entire operation is asynchronous.

Le code précédent montre que vous pouvez utiliser les objets Task ou Task<TResult> pour attendre les tâches en cours d’exécution.The preceding code showed you that you can use Task or Task<TResult> objects to hold running tasks. Vous attendez (await) la fin de chaque tâche avant d’utiliser ses résultats.You await each task before using its result. L’étape suivante consiste à créer des méthodes qui représentent la combinaison d’autres tâches.The next step is to create methods that represent the combination of other work. Avant de servir le petit-déjeuner, vous devez attendre la fin de la tâche qui correspond à l’action de faire griller le pain, avant d’ajouter le beurre et la confiture.Before serving breakfast, you want to await the task that represents toasting the bread before adding butter and jam. Vous pouvez représenter cette tâche à l’aide du code suivant :You can represent that work with the following code:

async Task<Toast> makeToastWithButterAndJamAsync(int number)
{
    var plainToast = await ToastBreadAsync(number);
    ApplyButter(plainToast);
    ApplyJam(plainToast);
    return plainToast;
}

La signature de la méthode précédente comprend le modificateur async.The preceding method has the async modifier in its signature. Cela signale au compilateur que cette méthode contient une instruction await. Elle contient des opérations asynchrones.That signals to the compiler that this method contains an await statement; it contains asynchronous operations. Cette méthode représente la tâche qui consiste à faire griller le pain, puis à ajouter le beurre et la confiture.This method represents the task that toasts the bread, then adds butter and jam. Cette méthode retourne un Task<TResult> qui représente la composition de ces trois opérations.This method returns a Task<TResult> that represents the composition of those three operations. Le bloc de code principal devient alors :The main block of code now becomes:

static async Task Main(string[] args)
{
    Coffee cup = PourCoffee();
    Console.WriteLine("coffee is ready");
    var eggsTask = FryEggsAsync(2);
    var baconTask = FryBaconAsync(3);
    var toastTask = makeToastWithButterAndJamAsync(2);

    var eggs = await eggsTask;
    Console.WriteLine("eggs are ready");
    var bacon = await baconTask;
    Console.WriteLine("bacon is ready");
    var toast = await toastTask;
    Console.WriteLine("toast is ready");
    Juice oj = PourOJ();
    Console.WriteLine("oj is ready");

    Console.WriteLine("Breakfast is ready!");

    async Task<Toast> makeToastWithButterAndJamAsync(int number)
    {
        var plainToast = await ToastBreadAsync(number);
        ApplyButter(plainToast);
        ApplyJam(plainToast);
        return plainToast;
    }
}

La modification précédente montre une technique importante pour l’utilisation du code asynchrone.The previous change illustrated an important technique for working with asynchronous code. Pour composer des tâches, vous devez séparer les opérations dans une nouvelle méthode qui retourne une tâche.You compose tasks by separating the operations into a new method that returns a task. Vous pouvez choisir dans quels cas attendre les tâches.You can choose when to await that task. Vous pouvez démarrer d’autres tâches simultanément.You can start other tasks concurrently.

Attendre efficacement la fin des tâchesAwait tasks efficiently

La série d’instructions await à la fin du code précédent peut être améliorée à l’aide des méthodes de la classe Task.The series of await statements at the end of the preceding code can be improved by using methods of the Task class. L’une de ces API est WhenAll, qui retourne un Task se terminant lorsque toutes les tâches de sa liste d’arguments sont terminées, comme indiqué dans le code suivant :One of those APIs is WhenAll, which returns a Task that completes when all the tasks in its argument list have completed, as shown in the following code:

await Task.WhenAll(eggTask, baconTask, toastTask);
Console.WriteLine("eggs are ready");
Console.WriteLine("bacon is ready");
Console.WriteLine("toast is ready");
Console.WriteLine("Breakfast is ready!");

Une autre option consiste à utiliser WhenAny, qui retourne un Task<Task> se terminant lorsque l’un de ses arguments se termine.Another option is to use WhenAny, which returns a Task<Task> that completes when any of its arguments completes. Vous pouvez attendre la tâche retournée, tout en sachant qu’elle est déjà terminée.You can await the returned task, knowing that it has already finished. Le code suivant montre comment utiliser WhenAny pour attendre la fin de la première tâche, puis traiter ses résultats.The following code shows how you could use WhenAny to await the first task to finish and then process its result. Après avoir traité les résultats de la tâche terminée, vous pouvez la supprimer de la liste des tâches passée à WhenAny.After processing the result from the completed task, you remove that completed task from the list of tasks passed to WhenAny.

var allTasks = new List<Task>{eggsTask, baconTask, toastTask};
while (allTasks.Any())
{
    Task finished = await Task.WhenAny(allTasks);
    if (finished == eggsTask)
    {
        Console.WriteLine("eggs are ready");
    }
    else if (finished == baconTask)
    {
        Console.WriteLine("bacon is ready");
    }
    else if (finished == toastTask)
    {
        Console.WriteLine("toast is ready");
    }
    allTasks.Remove(finished);
}
Console.WriteLine("Breakfast is ready!");

Après toutes ces modifications, la version finale de Main ressemble au code suivant :After all those changes, the final version of Main looks like the following code:

static async Task Main(string[] args)
{
    Coffee cup = PourCoffee();
    Console.WriteLine("coffee is ready");
    var eggsTask = FryEggsAsync(2);
    var baconTask = FryBaconAsync(3);
    var toastTask = makeToastWithButterAndJamAsync(2);

    var allTasks = new List<Task>{eggsTask, baconTask, toastTask};
    while (allTasks.Any())
    {
        Task finished = await Task.WhenAny(allTasks);
        if (finished == eggsTask)
        {
            Console.WriteLine("eggs are ready");
        }
        else if (finished == baconTask)
        {
            Console.WriteLine("bacon is ready");
        }
        else if (finished == toastTask)
        {
            Console.WriteLine("toast is ready");
        }
        allTasks.Remove(finished);
    }
    Console.WriteLine("Breakfast is ready!");

    async Task<Toast> makeToastWithButterAndJamAsync(int number)
    {
        var plainToast = await ToastBreadAsync(number);
        ApplyButter(plainToast);
        ApplyJam(plainToast);
        return plainToast;
    }
}

Le code final est asynchrone.This final code is asynchronous. Il reflète plus précisément la façon dont nous préparons habituellement le petit-déjeuner.It more accurately reflects how a person would cook a breakfast. Comparez le code précédent au premier exemple de code de cet article.Compare the preceding code with the first code sample in this article. Les actions de base sont toujours claires lorsque nous lisons le code.The core actions are still clear from reading the code. Ce code est tout aussi lisible que les instructions de préparation du petit-déjeuner au début de cet article.You can read this code the same way you'd read those instructions for making a breakfast at the beginning of this article. Les fonctionnalités de langage pour async et await traduisent ce que chaque personne fait lorsqu’elle suit des instructions : démarrer les tâches dès que possible et ne pas attendre qu’une tâche soit terminée avant de passer à la suivante.The language features for async and await provide the translation every person makes to follow those written instructions: start tasks as you can and don't block waiting for tasks to complete.