Modèle asynchrone basé sur les tâches dans .NET : introduction et vue d’ensemble

Dans .NET, le modèle asynchrone basé sur les tâches constitue le modèle de conception asynchrone recommandé pour les nouveaux développements. Il s’appuie sur les types Task et Task<TResult> de l’espace de noms System.Threading.Tasks, qui sont utilisés pour représenter les opérations asynchrones.

Noms, paramètres et types de retour

Le TAP utilise une méthode unique pour représenter le début et la fin d'une opération asynchrone. Ce fonctionnement le différencie du modèle de programmation asynchrone (APM ou IAsyncResult) et du modèle asynchrone basé sur les événements (EAP). Les méthodes Begin et End sont nécessaires pour le modèle APM. Le modèle EAP a besoin d’une méthode comportant le suffixe Async, ainsi que d’un ou plusieurs événements, de types de délégués de gestionnaire d’événements et de types dérivés de EventArg. Les méthodes asynchrones du modèle TAP qui retournent des types compatibles await, comme Task, Task<TResult>, ValueTask et ValueTask<TResult>, comprennent le suffixe Async après le nom de l’opération. Par exemple, une opération Get asynchrone qui retourne une valeur Task<String> peut porter le nom GetAsync. Si vous ajoutez une méthode TAP à une classe qui contient déjà un nom de méthode EAP avec le suffixe Async, utilisez plutôt le suffixe TaskAsync. Par exemple, si la classe possède déjà une méthode GetAsync, utilisez le nom GetTaskAsync. Les méthodes qui lancent une opération asynchrone, mais ne retournent pas de type compatible await, doivent porter un nom qui commence par Begin, Start ou un autre verbe suggérant qu’elles ne retournent ni ne lèvent le résultat de l’opération.  

Une méthode TAP retourne une System.Threading.Tasks.Task ou une System.Threading.Tasks.Task<TResult>, selon que la méthode synchrone correspondante retourne void ou un type TResult.

Les paramètres d’une méthode TAP doivent correspondre à ceux de son équivalent synchrone, dans le même ordre. Toutefois, les paramètres out et ref sont exempts de cette règle et doivent être évités entièrement. Les données qui auraient dû être retournées par un paramètre out ou ref doivent être retournées comme faisant partie du TResult retourné par Task<TResult>, et doivent utiliser un tuple ou une structure de données personnalisée pour s’adapter à plusieurs valeurs. Il peut aussi être intéressant d’ajouter un paramètre CancellationToken même si l’équivalent synchrone de la méthode du modèle asynchrone basé sur les tâches n’en offre pas.

Les méthodes qui sont consacrées exclusivement à la création, la manipulation ou la combinaison de tâches (où l’intention asynchrone de la méthode est clairement indiquée dans le nom de la méthode ou dans le nom du type auquel la méthode appartient) n’ont pas besoin de suivre ce modèle d’affectation de noms. Ces types de méthodes sont souvent appelés combinateurs. Les exemples de combinateurs incluent WhenAll et WhenAny, et sont traités dans la section Utilisation des combinateurs intégrés basés sur des tâches de l'article Utilisation du modèle asynchrone basé sur les tâches.

Pour obtenir des exemples de la façon dont la syntaxe du TAP diffère de celle utilisée dans les modèles de programmation asynchrones hérités tels que le modèle de programmation asynchrone (APM) et le modèle asynchrone basé sur des événements (EAP), consultez Modèles de programmation asynchrone.

Lancer une opération asynchrone

Une méthode asynchrone basée sur le TAP peut effectuer une petite quantité de travail de façon synchrone, comme valider les arguments et initialiser l’opération asynchrone, avant de retourner la tâche résultante. Le travail synchrone doit être conservé au minimum afin que la méthode asynchrone puisse retourner rapidement. Plusieurs raisons motivent un retour rapide :

  • Les méthodes asynchrones peuvent être appelées par des threads d'interface utilisateur, et tout travail synchrone de longue durée peut nuire à la réactivité de l'application.

  • Plusieurs méthodes asynchrones peuvent être lancées simultanément. Par conséquent, tout travail de longue durée dans la partie synchrone d'une méthode asynchrone risque de retarder l'initiation d'autres opérations asynchrones, réduisant ainsi les avantages offerts par l'accès concurrentiel.

Dans certains cas, la quantité de travail nécessaire pour terminer l'opération est inférieure à la quantité de travail nécessaire pour lancer l'opération de façon asynchrone. La lecture d'un flux où l'opération de lecture peut être satisfaite par les données déjà mises en mémoire tampon est un exemple de ce type de scénario. Dans de tels cas, l'opération peut s'exécuter de façon synchrone et peut retourner une tâche qui a déjà été effectuée.

Exceptions

Une méthode asynchrone doit lever une exception pour être rejetée de l'appel de méthode asynchrone uniquement en réponse à une erreur d'utilisation. Les erreurs d'utilisation ne doivent jamais se produire dans le code de production. Par exemple, si le fait de passer une référence null (Nothing en Visual Basic) en argument de la méthode provoque un état d’erreur (généralement représenté par une exception ArgumentNullException), il est possible de modifier le code appelant pour éviter qu’une référence null ne soit passée. Pour toutes les autres erreurs, les exceptions qui se produisent lorsqu'une méthode asynchrone est en cours d'exécution doivent être assignées à la tâche retournée, même si la méthode asynchrone se termine avant que la tâche ne soit retournée. En général, une tâche contient au plus une exception. Toutefois, si la tâche représente plusieurs opérations (par exemple, WhenAll), plusieurs exceptions peuvent être associées à une tâche unique.

Environnement cible

Lorsque vous implémentez une méthode TAP, il est possible de déterminer où l'exécution asynchrone se produit. Il est possible de choisir d’exécuter la charge de travail dans le pool de threads, de l’implémenter à l’aide d’E/S asynchrones (sans liaison à un thread pour la majorité de l’exécution de l’opération), de l’exécuter sur un thread spécifique (par exemple le thread d’interface utilisateur) ou d’utiliser plusieurs contextes potentiels. Une méthode TAP peut même ne rien avoir à exécuter et simplement retourner une Task qui représente l’occurrence d’une condition ailleurs dans le système (par exemple, une tâche qui représente des données reçues par une structure de données en file d’attente).

L’appelant de la méthode TAP peut bloquer l’attente de l’exécution de la méthode TAP par une attente synchrone sur la tâche obtenue, ou il peut exécuter du code supplémentaire (continuation) quand l’opération asynchrone est terminée. Le créateur du code de continuation a le contrôle sur l'endroit où ce code s'exécute. Il est possible de créer le code de continuation explicitement, via des méthodes sur la classe Task (par exemple, ContinueWith) ou implicitement, à l'aide de la prise en charge des langages par les continuations (par exemple, await en C#, Await en Visual Basic, AwaitValue en F#).

État des tâches

La classe Task fournit un cycle de vie pour les opérations asynchrones, et ce cycle est représenté par l'énumération TaskStatus. Pour prendre en charge les cas extrêmes de types qui dérivent de Task et de Task<TResult>, et pour prendre en charge la séparation de la construction et de la planification, la classe Task expose une méthode Start. Les tâches créées par les constructeurs Task publics sont connues sous le nom de tâches passives, car elles démarrent leur cycle de vie dans l'état Created non planifié et sont planifiées uniquement quand la méthode Start est appelée sur ces instances.

Toutes les autres tâches démarrent leur cycle de vie dans un état réactif, ce qui signifie que les opérations asynchrones qu’elles représentent ont déjà été initialisées et que leur état de tâche est une valeur d’énumération autre que TaskStatus.Created. Toutes les tâches qui sont retournées par les méthodes TAP doivent être activées. Si une méthode de modèle asynchrone basé sur les tâches utilise en interne le constructeur d’une tâche pour instancier la tâche à retourner, la méthode doit appeler Start sur l’objet Task avant de la retourner. Les consommateurs d’une méthode TAP peuvent sans risque supposer que la tâche retournée est active et ne doivent pas essayer d’appeler la méthode Start sur une Task qui est retournée à partir d’une méthode TAP. L'appel de la méthode Start sur une tâche active entraîne la levée d'une exception InvalidOperationException.

Annulation (facultatif)

Dans le TAP, l'annulation est facultative pour les implémenteurs de méthodes asynchrones et les consommateurs de méthodes asynchrones. Si une opération autorise l'annulation, elle expose une surcharge de la méthode asynchrone qui accepte un jeton d'annulation (instance de CancellationToken). Par convention, le paramètre est nommé cancellationToken.

public Task ReadAsync(byte [] buffer, int offset, int count,
                      CancellationToken cancellationToken)
Public Function ReadAsync(buffer() As Byte, offset As Integer,
                          count As Integer,
                          cancellationToken As CancellationToken) _
                          As Task

L'opération asynchrone surveille ce jeton en cas de demandes d'annulation. Si elle reçoit une demande d'annulation, elle peut choisir d'honorer cette demande et d'annuler l'opération. Si la demande d’annulation provoque l’arrêt prématuré du travail, la méthode TAP retourne une tâche qui se termine dans l’état Canceled ; il n’existe aucun résultat disponible et aucune exception n’est levée. L’état Canceled est considéré comme un état final (terminé) pour une tâche, il en est de même pour les états Faulted et RanToCompletion. Par conséquent, si une tâche est dans l'état Canceled, sa propriété IsCompleted retourne la valeur true. Lorsqu'une tâche se termine dans l'état Canceled, toutes les continuations enregistrées avec la tâche sont planifiées ou exécutées, sauf si une option de continuation telle que NotOnCanceled a été spécifiée pour quitter la continuation. Tout code qui attend de façon asynchrone une tâche annulée via l'utilisation de fonctionnalités de langage continue de s'exécuter mais reçoit une exception OperationCanceledException ou une exception qui en dérive. Le code qui est bloqué de manière synchrone en attendant la tâche via des méthodes telles que Wait et WaitAll continue également de s’exécuter avec une exception.

Si un jeton d’annulation a demandé l’annulation avant que la méthode TAP qui accepte le jeton ne soit appelée, la méthode TAP doit retourner une tâche Canceled. Toutefois, si l'annulation est demandée alors que l'opération asynchrone est en cours d'exécution, l'opération asynchrone n'a pas besoin d'accepter la demande d'annulation. La tâche retournée doit se terminer dans l’état Canceled uniquement si l’opération se termine à la suite de la demande d’annulation. Si l'annulation est demandée mais qu'un résultat (ou une exception) est tout de même produit, la tâche doit se terminer dans l'état RanToCompletion ou Faulted.

Dans le cas des méthodes asynchrones dont la capacité à être annulées est prioritaire, vous ne devez pas fournir de surcharge n’acceptant pas de jeton d’annulation. Pour les méthodes qui ne peuvent pas être annulées, ne fournissez aucune surcharge acceptant un jeton d'annulation ; cela permet d'indiquer à l'appelant si la méthode cible est réellement annulable ou non. Le code de consommateur qui ne souhaite pas l’annulation peut appeler une méthode qui accepte un CancellationToken et fournit None comme valeur d’argument. None est fonctionnellement équivalent au CancellationToken par défaut.

Rapport de progression (facultatif)

Certaines opérations asynchrones tirent parti de la fourniture de notifications de progression ; elles sont généralement utilisées pour mettre à jour une interface utilisateur avec des informations sur la progression de l'opération asynchrone.

Dans le TAP, la progression est gérée via une interface IProgress<T>, qui est passée à la méthode asynchrone en tant que paramètre généralement nommé progress. Le fait de fournir l'interface de progression lorsque la méthode asynchrone est appelée aide à éliminer les conditions de concurrence qui résultent d'une utilisation incorrecte (autrement dit, lorsque les gestionnaires d'événements enregistrés de façon incorrecte après le démarrage de l'opération sont susceptibles de manquer des mises à jour). Plus important encore, l'interface de progression prend en charge diverses implémentations de progression, comme indiqué par le code de consommation. Par exemple, le code de consommation peut s’intéresser uniquement à la dernière mise à jour de la progression, vouloir placer toutes les mises à jour en mémoire tampon, chercher à appeler une action pour chaque mise à jour ou encore s’attacher à vérifier si l’appel est marshalé à un thread en particulier. Toutes ces options sont réalisables avec une implémentation différente de l’interface, personnalisée en fonction des besoins particuliers du consommateur. Comme pour l'annulation, les implémentations du TAP doivent fournir un paramètre IProgress<T> uniquement si l'API prend en charge les notifications de progression.

Par exemple, si la méthode ReadAsync décrite précédemment dans cet article peut indiquer la progression intermédiaire sous la forme du nombre d'octets lus jusqu'ici, le rappel de progression peut être une interface IProgress<T> :

public Task ReadAsync(byte[] buffer, int offset, int count,
                      IProgress<long> progress)
Public Function ReadAsync(buffer() As Byte, offset As Integer,
                          count As Integer,
                          progress As IProgress(Of Long)) As Task

Si une méthode FindFilesAsync retourne la liste de tous les fichiers qui suivent un modèle de recherche spécifique, le rappel de progression peut fournir une estimation du pourcentage de travail effectué et l’ensemble actuel des résultats partiels. Il est possible de transmettre ces informations par le biais d’un tuple :

public Task<ReadOnlyCollection<FileInfo>> FindFilesAsync(
            string pattern,
            IProgress<Tuple<double,
            ReadOnlyCollection<List<FileInfo>>>> progress)
Public Function FindFilesAsync(pattern As String,
                               progress As IProgress(Of Tuple(Of Double, ReadOnlyCollection(Of List(Of FileInfo))))) _
                               As Task(Of ReadOnlyCollection(Of FileInfo))

Cela peut également s’effectuer avec un type de données propre à l’API :

public Task<ReadOnlyCollection<FileInfo>> FindFilesAsync(
    string pattern,
    IProgress<FindFilesProgressInfo> progress)
Public Function FindFilesAsync(pattern As String,
                               progress As IProgress(Of FindFilesProgressInfo)) _
                               As Task(Of ReadOnlyCollection(Of FileInfo))

Dans le second cas, le type de données particulier possède généralement le suffixe ProgressInfo.

Si les implémentations du modèle asynchrone basé sur les tâches fournissent des surcharges qui acceptent un paramètre progress, elles doivent permettre à l’argument d’être null. Dans ce cas, aucune progression n’est signalée. Les implémentations du modèle asynchrone basé sur les tâches doivent signaler la progression vers l’objet Progress<T> de façon synchrone, ce qui permet à la méthode asynchrone de fournir rapidement la progression. Le consommateur de la progression peut par ailleurs déterminer où et comment gérer au mieux l’information. Par exemple, l'instance de progression peut choisir de marshaler les rappels et de déclencher des événements dans un contexte de synchronisation capturé.

Implémentations IProgress<T>

.NET fournit la classe Progress<T>, qui implémente IProgress<T>. La classe Progress<T> est déclarée comme suit :

public class Progress<T> : IProgress<T>  
{  
    public Progress();  
    public Progress(Action<T> handler);  
    protected virtual void OnReport(T value);  
    public event EventHandler<T>? ProgressChanged;  
}  

Une instance de Progress<T> expose un événement ProgressChanged, qui est déclenché chaque fois que l'opération asynchrone signale une mise à jour de la progression. L'événement ProgressChanged est déclenché sur l'objet SynchronizationContext qui a été capturé lorsque l'instance de Progress<T> a été instanciée. Si aucun contexte de synchronisation n’est disponible, un contexte par défaut qui cible le pool de threads est utilisé. Les gestionnaires peuvent être enregistrés avec cet événement. Un gestionnaire unique peut également être fourni au constructeur Progress<T> pour des raisons pratiques, et se comporter comme un gestionnaire d'événements pour l'événement ProgressChanged. Les mises à jour de progression sont déclenchées de façon asynchrone afin d'éviter de différer l'opération asynchrone alors que les gestionnaires d'événements s'exécutent. Une autre implémentation de IProgress<T> peut choisir d'appliquer une sémantique différente.

Choisir les surcharges à fournir

Si une implémentation du TAP utilise des paramètres facultatifs CancellationToken et IProgress<T>, elle est susceptible de nécessiter jusqu'à quatre surcharges :

public Task MethodNameAsync(…);  
public Task MethodNameAsync(…, CancellationToken cancellationToken);  
public Task MethodNameAsync(…, IProgress<T> progress);
public Task MethodNameAsync(…,
    CancellationToken cancellationToken, IProgress<T> progress);  
Public MethodNameAsync(…) As Task  
Public MethodNameAsync(…, cancellationToken As CancellationToken cancellationToken) As Task  
Public MethodNameAsync(…, progress As IProgress(Of T)) As Task
Public MethodNameAsync(…, cancellationToken As CancellationToken,
                       progress As IProgress(Of T)) As Task  

Toutefois, plusieurs implémentations du modèle asynchrone basé sur les tâches ne fournissent ni capacité d’annulation ni capacité de progression. Par conséquent, seule une méthode est nécessaire :

public Task MethodNameAsync(…);  
Public MethodNameAsync(…) As Task  

Si une implémentation du TAP prend en charge l'annulation ou la progression, mais pas les deux, elle peut fournir deux surcharges :

public Task MethodNameAsync(…);  
public Task MethodNameAsync(…, CancellationToken cancellationToken);  
  
// … or …  
  
public Task MethodNameAsync(…);  
public Task MethodNameAsync(…, IProgress<T> progress);  
Public MethodNameAsync(…) As Task  
Public MethodNameAsync(…, cancellationToken As CancellationToken) As Task  
  
' … or …  
  
Public MethodNameAsync(…) As Task  
Public MethodNameAsync(…, progress As IProgress(Of T)) As Task  

Si une implémentation du TAP prend en charge l'annulation et la progression, elle peut exposer les quatre surcharges. Toutefois, elle peut fournir uniquement les deux suivantes :

public Task MethodNameAsync(…);  
public Task MethodNameAsync(…,
    CancellationToken cancellationToken, IProgress<T> progress);  
Public MethodNameAsync(…) As Task  
Public MethodNameAsync(…, cancellationToken As CancellationToken,
                       progress As IProgress(Of T)) As Task  

Pour compenser les deux combinaisons intermédiaires manquantes, les développeurs peuvent passer None ou un CancellationToken par défaut pour le paramètre cancellationToken et la valeur null pour le paramètre progress.

Si vous vous attendez à ce que chaque utilisation de la méthode du modèle asynchrone basé sur les tâches prenne en charge l’annulation ou la progression, vous pouvez omettre les surcharges qui n’acceptent pas le paramètre approprié.

Si vous décidez d’exposer plusieurs surcharges pour rendre l’annulation ou la progression facultative, celles qui sont incompatibles avec l’annulation ou la progression doivent se comporter comme si elles passaient None pour l’annulation ou null pour la progression à la surcharge qui les accepte.

Intitulé Description
Modèles de programmation asynchrone Présente les trois modèles permettant d'effectuer des opérations asynchrones : le modèle asynchrone basé sur des tâches (TAP), le modèle de programmation asynchrone (APM) et le modèle asynchrone basé sur des événements (EAP).
Implémentation du modèle asynchrone basé sur des tâches Décrit comment implémenter le modèle asynchrone basé sur des tâches (TAP) de trois façons : à l'aide des compilateurs C# et Visual Basic dans Visual Studio, manuellement, ou par une combinaison des méthodes du compilateur et des méthodes manuelles.
Utilisation du modèle asynchrone basé sur les tâches Décrit comment utiliser des tâches et des rappels afin de terminer l’attente sans blocage.
Interopérabilité avec d’autres types et modèles asynchrones Décrit comment utiliser le modèle asynchrone basé sur des tâches (TAP) pour implémenter le modèle de programmation asynchrone (APM) et le modèle asynchrone basé sur des événements (EAP).