Async en détailAsync in depth

L’écriture de code asynchrone utilisant des E/S et le processeur est simple avec le modèle asynchrone .NET basé sur des tâches.Writing I/O- and CPU-bound asynchronous code is straightforward using the .NET Task-based async model. Le modèle est exposé par les types Task et Task<T> et les mots clés async et await en C# et Visual Basic.The model is exposed by the Task and Task<T> types and the async and await keywords in C# and Visual Basic. (Les ressources spécifiques à une langue se trouvent dans la section Voir aussi .) Cet article explique comment utiliser .NET Async et fournit des informations sur l’environnement Async utilisé en coulisses.(Language-specific resources are found in the See also section.) This article explains how to use .NET async and provides insight into the async framework used under the covers.

Tâche et tâche<T>Task and Task<T>

Les tâches sont des constructions utilisées pour implémenter ce que l’on appelle le modèle de promesses de concurrence.Tasks are constructs used to implement what is known as the Promise Model of Concurrency. En bref, elles vous offrent la « promesse » que le travail sera terminé à un moment ultérieur, ce qui vous permet de coordonner la promesse et une nouvelle API.In short, they offer you a "promise" that work will be completed at a later point, letting you coordinate with the promise with a clean API.

  • Task représente une opération unique qui ne retourne pas de valeur.Task represents a single operation which does not return a value.
  • Task<T> représente une opération unique qui retourne une valeur de type T.Task<T> represents a single operation which returns a value of type T.

Il est important de considérer les tâches comme des abstractions de travail effectuées de manière asynchrone et pas comme une abstraction sur le modèle de thread.It’s important to reason about tasks as abstractions of work happening asynchronously, and not an abstraction over threading. Par défaut, les tâches s’exécutent sur le thread actuel et délèguent le travail au système d’exploitation, comme il convient.By default, tasks execute on the current thread and delegate work to the Operating System, as appropriate. Éventuellement, l’API Task.Run peut servir à demander explicitement aux tâches de s’exécuter sur un thread distinct.Optionally, tasks can be explicitly requested to run on a separate thread via the Task.Run API.

Les tâches exposent un protocole d’API pour surveiller et attendre la valeur de résultat d’une tâche et y accéder (dans le cas de Task<T>).Tasks expose an API protocol for monitoring, waiting upon and accessing the result value (in the case of Task<T>) of a task. L’intégration au langage, avec le mot clé await, fournit une abstraction de niveau supérieur pour l’utilisation des tâches.Language integration, with the await keyword, provides a higher-level abstraction for using tasks.

L’utilisation de await permet à votre application ou service d’effectuer un travail utile pendant l’exécution d’une tâche en cédant le contrôle à son appelant jusqu’à ce que la tâche soit terminée.Using await allows your application or service to perform useful work while a task is running by yielding control to its caller until the task is done. Votre code n’a pas besoin de s’appuyer sur des rappels ou des événements pour continuer l’exécution une fois la tâche terminée.Your code does not need to rely on callbacks or events to continue execution after the task has been completed. L’intégration des API de langage et de tâche s’en charge pour vous.The language and task API integration does that for you. Si vous utilisez Task<T>, le mot clé await « désencapsule » également la valeur retournée quand la tâche est terminée.If you’re using Task<T>, the await keyword will additionally "unwrap" the value returned when the Task is complete. Les détails de ce fonctionnement sont expliqués plus bas.The details of how this works are explained further below.

Pour en savoir plus sur les tâches et les différentes façons d’interagir avec elles, lisez la rubrique Modèle asynchrone basé sur les tâches (TAP, Task-based Asynchronous Pattern).You can learn more about tasks and the different ways to interact with them in the Task-based Asynchronous Pattern (TAP) topic.

Approfondissement : Tâches pour une opération utilisant des E/SDeeper Dive into Tasks for an I/O-Bound Operation

La section suivante décrit en détail toutes les étapes d’un appel d’E/S asynchrone standard.The following section describes a 10,000 foot view of what happens with a typical async I/O call. Commençons par deux exemples.Let's start with a couple examples.

Le premier exemple appelle une méthode async et retourne une tâche active qui n’est pas encore terminée.The first example calls an async method and returns an active task, likely yet to complete.

public Task<string> GetHtmlAsync()
{
    // Execution is synchronous here
    var client = new HttpClient();

    return client.GetStringAsync("https://www.dotnetfoundation.org");
}

Le deuxième exemple ajoute l’utilisation des mots clés async et await pour agir sur la tâche.The second example adds the use of the async and await keywords to operate on the task.

public async Task<string> GetFirstCharactersCountAsync(string url, int count)
{
    // Execution is synchronous here
    var client = new HttpClient();

    // Execution of GetFirstCharactersCountAsync() is yielded to the caller here
    // GetStringAsync returns a Task<string>, which is *awaited*
    var page = await client.GetStringAsync("https://www.dotnetfoundation.org");

    // Execution resumes when the client.GetStringAsync task completes,
    // becoming synchronous again.

    if (count > page.Length)
    {
        return page;
    }
    else
    {
        return page.Substring(0, count);
    }
}

L’appel de GetStringAsync() s’effectue par le biais de bibliothèques .NET de niveau inférieur (peut-être en appelant d’autres méthodes async) jusqu’à ce qu’il atteigne un appel interop P/Invoke dans une bibliothèque de réseau native.The call to GetStringAsync() calls through lower-level .NET libraries (perhaps calling other async methods) until it reaches a P/Invoke interop call into a native networking library. La bibliothèque native peut ensuite effectuer un appel de l’API système (tel que write() pour un socket sur Linux).The native library may subsequently call into a System API call (such as write() to a socket on Linux). Un objet de tâche est créé dans la limite native/managée, éventuellement à l’aide de TaskCompletionSource.A task object will be created at the native/managed boundary, possibly using TaskCompletionSource. L’objet de tâche est transmis à travers les couches, éventuellement traité ou directement retourné, ou retourné à l’appelant initial.The task object will be passed up through the layers, possibly operated on or directly returned, eventually returned to the initial caller.

Dans le deuxième exemple ci-dessus, un objet Task<T> est retourné par GetStringAsync.In the second example above, a Task<T> object will be returned from GetStringAsync. L’utilisation du mot clé await indique à la méthode de retourner un objet de tâche nouvellement créé.The use of the await keyword causes the method to return a newly created task object. Le contrôle retourne à l’appelant à partir de cet emplacement dans la méthode GetFirstCharactersCountAsync.Control returns to the caller from this location in the GetFirstCharactersCountAsync method. Les méthodes et propriétés de l’objet Task<T> permettent aux appelants de surveiller la progression de la tâche, qui se termine quand le code restant dans GetFirstCharactersCountAsync a été exécuté.The methods and properties of the Task<T> object enable callers to monitor the progress of the task, which will complete when the remaining code in GetFirstCharactersCountAsync has executed.

Après l’appel de l’API système, la demande se trouve dans l’espace du noyau et transite vers le sous-système de réseau du système d’exploitation (comme /net dans le noyau Linux).After the System API call, the request is now in kernel space, making its way to the networking subsystem of the OS (such as /net in the Linux Kernel). Ici, le système d’exploitation gère la demande de mise en réseau de manière asynchrone.Here the OS will handle the networking request asynchronously. Les détails peuvent être différents selon le système d’exploitation utilisé (l’appel du pilote de périphérique peut être planifié comme un signal envoyé au runtime, ou il peut être effectué et ensuite un signal est renvoyé), mais finalement le runtime est informé que la demande de mise en réseau est en cours.Details may be different depending on the OS used (the device driver call may be scheduled as a signal sent back to the runtime, or a device driver call may be made and then a signal sent back), but eventually the runtime will be informed that the networking request is in progress. À ce stade, le travail du pilote de périphérique est planifié, en cours ou déjà terminé (la demande est déjà « sur le réseau »), mais parce que tout se passe de manière asynchrone, le pilote de périphérique est en mesure de gérer immédiatement autre chose !At this time, the work for the device driver will either be scheduled, in-progress, or already finished (the request is already out "over the wire") - but because this is all happening asynchronously, the device driver is able to immediately handle something else!

Par exemple, dans Windows, un thread de système d’exploitation effectue un appel au pilote de périphérique réseau et lui demande d’effectuer l’opération de mise en réseau via un paquet de requêtes d’interruption qui représente l’opération.For example, in Windows an OS thread makes a call to the network device driver and asks it to perform the networking operation via an Interrupt Request Packet (IRP) which represents the operation. Le pilote de périphérique reçoit le paquet de requêtes d’interruption, effectue l’appel au réseau, marque le paquet comme étant « en attente » et le renvoie au système d’exploitation.The device driver receives the IRP, makes the call to the network, marks the IRP as "pending", and returns back to the OS. Le thread du système d’exploitation sait maintenant que le paquet de requêtes d’interruption est « en attente », il n’a donc rien d’autre à faire pour ce travail et « revient » pour pouvoir être utilisé pour une autre opération.Because the OS thread now knows that the IRP is "pending", it doesn't have any more work to do for this job and "returns" back so that it can be used to perform other work.

Quand la demande est satisfaite et que les données reviennent à travers le pilote de périphérique, il avertit le processeur que de nouvelles données sont reçues via une interruption.When the request is fulfilled and data comes back through the device driver, it notifies the CPU of new data received via an interrupt. La façon dont cette interruption est gérée varie selon le système d’exploitation, mais les données sont ensuite transmises au système d’exploitation jusqu’à ce que se produise un appel d’interopérabilité système (par exemple, dans Linux, un gestionnaire d’interruptions planifie la moitié inférieure de l’IRQ pour qu’elle transmette les données via le système d’exploitation de façon asynchrone).How this interrupt gets handled will vary depending on the OS, but eventually the data will be passed through the OS until it reaches a system interop call (for example, in Linux an interrupt handler will schedule the bottom half of the IRQ to pass the data up through the OS asynchronously). Notez que cela se produit également de façon asynchrone !Note that this also happens asynchronously! Le résultat est placé en file d’attente jusqu’à ce que le prochain thread disponible soit en mesure d’exécuter la méthode asynchrone et de « désencapsuler » le résultat de la tâche effectuée.The result is queued up until the next available thread is able to execute the async method and "unwrap" the result of the completed task.

Tout au long de ce processus, un élément clé à retenir est qu’aucun thread n’est dédié à l’exécution de la tâche.Throughout this entire process, a key takeaway is that no thread is dedicated to running the task. Bien que le travail soit exécuté dans un contexte (c’est-à-dire que le système d’exploitation doit passer des données à un pilote de périphérique et répondre à une interruption), aucun thread n’est destiné à attendre le retour des données de la demande.Although work is executed in some context (that is, the OS does have to pass data to a device driver and respond to an interrupt), there is no thread dedicated to waiting for data from the request to come back. Cela permet au système de gérer une plus grande quantité de travail au lieu d’attendre la fin des appels d’E/S.This allows the system to handle a much larger volume of work rather than waiting for some I/O call to finish.

Bien que les étapes ci-dessus puissent donner l’impression d’un grand nombre d’opérations à effectuer, en termes de durée totale d’exécution, ce n’est rien comparé au temps nécessaire pour effectuer le travail d’E/S réel.Although the above may seem like a lot of work to be done, when measured in terms of wall clock time, it’s miniscule compared to the time it takes to do the actual I/O work. Voici une vague idée de ce que pourrait être la chronologie de ces étapes :Although not at all precise, a potential timeline for such a call would look like this:

0-1————————————————————————————————————————————————–2-30-1————————————————————————————————————————————————–2-3

  • La durée entre les points 0 et 1 représente tout ce qui se passe avant qu’une méthode async cède le contrôle à son appelant.Time spent from points 0 to 1 is everything up until an async method yields control to its caller.
  • La durée entre les points 1 et 2 représente le temps consacré aux E/S, sans coût de processeur.Time spent from points 1 to 2 is the time spent on I/O, with no CPU cost.
  • Enfin, la durée entre les points 2 et 3 représente le temps consacré à rendre le contrôle (et éventuellement une valeur) à la méthode async, moment à partir duquel elle s’exécute à nouveau.Finally, time spent from points 2 to 3 is passing control back (and potentially a value) to the async method, at which point it is executing again.

Qu’est-ce que cela signifie dans un scénario de serveur ?What does this mean for a server scenario?

Ce modèle fonctionne correctement avec la charge de travail d’un scénario de serveur classique.This model works well with a typical server scenario workload. Comme aucun thread n’est destiné à s’interrompre sur les tâches non terminées, le pool de threads serveur peut traiter un plus grand nombre de demandes web.Because there are no threads dedicated to blocking on unfinished tasks, the server threadpool can service a much higher volume of web requests.

Prenons deux serveurs : l’un d’eux exécute du code asynchrone et l’autre pas.Consider two servers: one that runs async code, and one that does not. Pour les besoins de cet exemple, chaque serveur n’a que 5 threads disponibles pour traiter les demandes.For the purpose of this example, each server only has 5 threads available to service requests. Notez que ces chiffres sont intentionnellement petits et servent uniquement dans un contexte de démonstration.Note that these numbers are imaginarily small and serve only in a demonstrative context.

Supposons que les deux serveurs reçoivent 6 demandes simultanées.Assume both servers receive 6 concurrent requests. Chaque demande effectue une opération d’E/S.Each request performs an I/O operation. Le serveur sans code asynchrone doit placer en file d’attente la 6ème demande jusqu’à ce que l’un des 5 threads ait terminé le travail utilisant des E/S et écrit une réponse.The server without async code has to queue up the 6th request until one of the 5 threads have finished the I/O-bound work and written a response. Quand la 20ème demande arrive, le serveur commence peut-être à ralentir, car la file d’attente devient trop longue.At the point that the 20th request comes in, the server might start to slow down, because the queue is getting too long.

Le serveur avec code asynchrone peut placer en file d’attente la 6ème demande, mais parce qu’il utilise async et await, chacun de ses threads est libéré quand le travail utilisant des E/S démarre, et non quand il se termine.The server with async code running on it still queues up the 6th request, but because it uses async and await, each of its threads are freed up when the I/O-bound work starts, rather than when it finishes. Quand la 20ème demande arrive, la file d’attente des demandes entrantes est bien plus petite (ou est totalement vide) et le serveur ne ralentit pas.By the time the 20th request comes in, the queue for incoming requests will be far smaller (if it has anything in it at all), and the server won't slow down.

Bien qu’il s’agisse d’un exemple fictif, il fonctionne de manière très similaire dans le monde réel.Although this is a contrived example, it works in a very similar fashion in the real world. En réalité, un serveur est capable de gérer un nombre bien plus important de demandes à l’aide de async et await que s’il dédiait un thread à chaque demande qu’il reçoit.In fact, you can expect a server to be able to handle an order of magnitude more requests using async and await than if it were dedicating a thread for each request it receives.

Qu’est-ce que cela signifie dans un scénario de client ?What does this mean for client scenario?

Le plus gros avantage de l’utilisation de async et await pour une application cliente est l’augmentation de la réactivité.The biggest gain for using async and await for a client app is an increase in responsiveness. Même si vous pouvez améliorer la réactivité d’une application en gérant manuellement des threads de manière dynamique, c’est une opération coûteuse par rapport à la simple utilisation de async et await.Although you can make an app responsive by spawning threads manually, the act of spawning a thread is an expensive operation relative to just using async and await. Dans le cas particulier d’un jeu mobile, il est essentiel d’affecter aussi peu que possible le thread d’interface utilisateur en ce qui concerne les E/S.Especially for something like a mobile game, impacting the UI thread as little as possible where I/O is concerned is crucial.

Plus important encore, parce que le travail utilisant des E/S ne se sert pratiquement pas du processeur, en dédiant un thread de processeur entier pour effectuer vaguement des tâches utiles, vous utilisez mal vos ressources.More importantly, because I/O-bound work spends virtually no time on the CPU, dedicating an entire CPU thread to perform barely any useful work would be a poor use of resources.

Par ailleurs, la répartition du travail sur le thread d’interface utilisateur (par exemple, la mise à jour d’une interface utilisateur) est très simple avec des méthodes async et n’engendre pas de travail supplémentaire (par exemple, l’appel d’un délégué thread-safe).Additionally, dispatching work to the UI thread (such as updating a UI) is very simple with async methods, and does not require extra work (such as calling a thread-safe delegate).

Approfondissement : Task et Task<T> pour une opération utilisant le processeurDeeper Dive into Task and Task<T> for a CPU-Bound Operation

Le code async utilisant le processeur est un peu différent du code async utilisant des E/S.CPU-bound async code is a bit different than I/O-bound async code. Comme le travail est effectué sur le processeur, il n’est pas possible de dédier un thread au calcul.Because the work is done on the CPU, there's no way to get around dedicating a thread to the computation. L’utilisation de async et await est un moyen d’interagir avec un thread en arrière-plan et de faire en sorte que l’appelant de la méthode async reste réactif.The use of async and await provides you with a clean way to interact with a background thread and keep the caller of the async method responsive. Notez que cela ne protège en rien les données partagées.Note that this does not provide any protection for shared data. Si vous utilisez des données partagées, vous devez quand même appliquer une stratégie de synchronisation appropriée.If you are using shared data, you will still need to apply an appropriate synchronization strategy.

Voici une vue générale d’un appel asynchrone utilisant le processeur :Here's a 10,000 foot view of a CPU-bound async call:

public async Task<int> CalculateResult(InputData data)
{
    // This queues up the work on the threadpool.
    var expensiveResultTask = Task.Run(() => DoExpensiveCalculation(data));

    // Note that at this point, you can do some other work concurrently,
    // as CalculateResult() is still executing!

    // Execution of CalculateResult is yielded here!
    var result = await expensiveResultTask;

    return result;
}

CalculateResult() s’exécute sur le thread sur lequel il a été appelé.CalculateResult() executes on the thread it was called on. Quand il appelle Task.Run, il place en file d’attente l’opération coûteuse qui utilise le processeur, DoExpensiveCalculation(), sur le pool de threads et reçoit un handle Task<int>.When it calls Task.Run, it queues the expensive CPU-bound operation, DoExpensiveCalculation(), on the thread pool and receives a Task<int> handle. DoExpensiveCalculation() est finalement exécuté simultanément sur le prochain thread disponible, probablement sur un autre cœur d’UC.DoExpensiveCalculation() is eventually run concurrently on the next available thread, likely on another CPU core. Il est possible d’effectuer des tâches simultanées quand DoExpensiveCalculation() est occupé sur un autre thread, car le thread qui a appelé CalculateResult() est encore en cours d’exécution.It's possible to do concurrent work while DoExpensiveCalculation() is busy on another thread, because the thread which called CalculateResult() is still executing.

Une fois que await a été trouvé, l’exécution de CalculateResult() est cédée à son appelant, ce qui permet d’effectuer d’autres tâches avec le thread actuel pendant que DoExpensiveCalculation() produit un résultat.Once await is encountered, the execution of CalculateResult() is yielded to its caller, allowing other work to be done with the current thread while DoExpensiveCalculation() is churning out a result. Une fois cette opération terminée, le résultat est placé en file d’attente pour s’exécuter sur le thread principal.Once it has finished, the result is queued up to run on the main thread. Finalement, le thread principal retourne à l’exécution de CalculateResult(), à partir duquel il obtient le résultat de DoExpensiveCalculation().Eventually, the main thread will return to executing CalculateResult(), at which point it will have the result of DoExpensiveCalculation().

Pourquoi async est-il utile ici ?Why does async help here?

async et await représentent la meilleure pratique de gestion des travaux utilisant le processeur de manière intensive en cas d’impératifs de réactivité.async and await are the best practice for managing CPU-bound work when you need responsiveness. Il existe plusieurs modèles d’utilisation d’async avec des tâches utilisant le processeur.There are multiple patterns for using async with CPU-bound work. Notez que l’utilisation d’async représente un coût, même s’il est faible, et qu’elle n’est donc pas recommandée pour les boucles serrées.It's important to note that there is a small cost to using async and it's not recommended for tight loops. C’est à vous de déterminer la façon dont vous écrivez votre code autour de cette nouvelle fonctionnalité.It's up to you to determine how you write your code around this new capability.

Voir aussiSee also