Implémentation du modèle asynchrone basé sur des tâches

Vous pouvez implémenter le modèle asynchrone basé sur des tâches (TAP) de trois façons : en utilisant les compilateurs C# et Visual Basic dans Visual Studio, manuellement ou via une combinaison du compilateur et des méthodes manuelles. Les sections suivantes décrivent chaque méthode en détail. Vous pouvez utiliser le modèle TAP pour implémenter des opérations asynchrones liées au calcul et liées aux E/S. La section Charges de travail décrit chaque type d’opération.

Génération de méthodes TAP

Utilisation des compilateurs

À compter de .NET Framework 4.5, toute méthode attribuée avec le mot clé async (Async dans Visual Basic) est considérée comme une méthode asynchrone. Les compilateurs C# et Visual Basic effectuent les transformations nécessaires pour implémenter la méthode de manière asynchrone à l’aide de TAP. Une méthode asynchrone doit retourner un System.Threading.Tasks.Task ou un System.Threading.Tasks.Task<TResult> objet. Pour ce dernier, le corps de la fonction doit retourner un TResult, et le compilateur garantit que ce résultat est rendu disponible via l’objet de tâche résultant. De même, toutes les exceptions qui ne sont pas gérées dans le corps de la méthode sont marshalées vers la tâche de sortie et provoquent la fin de la tâche résultante dans l’état TaskStatus.Faulted . L’exception à cette règle est lorsqu’un OperationCanceledException (ou type dérivé) n’est pas géré, auquel cas la tâche résultante se termine dans l’état TaskStatus.Canceled .

Tâche.Start et suppression des tâches

Utilisez Start uniquement pour les tâches explicitement créées avec un constructeur Task qui sont toujours dans l’état Created. Les méthodes publiques TAP devraient renvoyer des tâches actives, de sorte que les appelants n'aient pas besoin d'appeler Start.

Dans la plupart des codes TAP, ne supprimez pas les tâches. Un Task ne contient généralement pas de ressources non managées, et la libération de chaque tâche ajoute un surcoût sans avantage pratique. Éliminez uniquement lorsque des API ou des mesures spécifiques indiquent un besoin.

Si vous démarrez un travail en arrière-plan qui dépasse le chemin d’appel immédiat, conservez la propriété explicite et suivez l’achèvement. Pour plus d’informations, consultez Conserver les méthodes asynchrones actives.

Génération manuelle de méthodes TAP

Vous pouvez implémenter manuellement le modèle TAP pour mieux contrôler l’implémentation. Le compilateur s'appuie sur la surface publique exposée depuis l'espace de noms System.Threading.Tasks et sur les types de prise en charge de l'espace de noms System.Runtime.CompilerServices. Pour implémenter le TAP vous-même, vous créez un TaskCompletionSource<TResult> objet, effectuez l’opération asynchrone et, quand elle se termine, appelez la SetResultméthode SetException, ou SetCanceled la Try version de l’une de ces méthodes. Lorsque vous implémentez manuellement une méthode TAP, vous devez effectuer la tâche résultante lorsque l’opération asynchrone représentée se termine. Par exemple:

static class StreamExtensions
{
    public static Task<int> ReadTask(this Stream stream, byte[] buffer, int offset, int count, object? state)
    {
        var tcs = new TaskCompletionSource<int>();
        stream.BeginRead(buffer, offset, count, ar =>
        {
            try { tcs.SetResult(stream.EndRead(ar)); }
            catch (Exception exc) { tcs.SetException(exc); }
        }, state);
        return tcs.Task;
    }
}
Module StreamExtensions
    <Extension()>
    Public Function ReadTask(stream As Stream, buffer As Byte(),
                             offset As Integer, count As Integer,
                             state As Object) As Task(Of Integer)
        Dim tcs As New TaskCompletionSource(Of Integer)()
        stream.BeginRead(buffer, offset, count,
            Sub(ar)
                Try
                    tcs.SetResult(stream.EndRead(ar))
                Catch exc As Exception
                    tcs.SetException(exc)
                End Try
            End Sub, state)
        Return tcs.Task
    End Function
End Module

Approche hybride

Vous pouvez trouver utile d’implémenter le modèle TAP manuellement, mais de déléguer la logique principale de l’implémentation au compilateur. Par exemple, vous pouvez utiliser l’approche hybride lorsque vous souhaitez vérifier les arguments en dehors d’une méthode asynchrone générée par le compilateur afin que les exceptions puissent s’échapper à l’appelant direct de la méthode plutôt que d’être exposées via l’objet System.Threading.Tasks.Task :

class Calculator
{
    private int value = 0;

    public Task<int> MethodAsync(string input)
    {
        if (input == null) throw new ArgumentNullException(nameof(input));
        return MethodAsyncInternal(input);
    }

    private async Task<int> MethodAsyncInternal(string input)
    {
        // code that uses await goes here
        await Task.Delay(1);
        return value;
    }
}
Class Calculator
    Private value As Integer = 0

    Public Function MethodAsync(input As String) As Task(Of Integer)
        If input Is Nothing Then Throw New ArgumentNullException(NameOf(input))
        Return MethodAsyncInternal(input)
    End Function

    Private Async Function MethodAsyncInternal(input As String) As Task(Of Integer)
        ' code that uses await goes here
        Await Task.Delay(1)
        Return value
    End Function
End Class

Un autre cas où cette délégation est utile est lorsque vous implémentez l’optimisation du chemin rapide et que vous souhaitez retourner une tâche mise en cache.

Charges de travail

Vous pouvez implémenter des opérations asynchrones liées au calcul et liées aux E/S en tant que méthodes TAP. Toutefois, lorsque vous exposez publiquement des méthodes TAP à partir d’une bibliothèque, fournissez-les uniquement pour les charges de travail qui impliquent des opérations liées aux E/S. Ces opérations peuvent également impliquer le calcul, mais elles ne doivent pas être purement informatiques. Si une méthode est purement liée au calcul, exposez-la uniquement en tant qu’implémentation synchrone. Le code qui l’utilise peut ensuite choisir d’encapsuler une invocation de cette méthode synchrone dans une tâche pour décharger le travail vers un autre thread ou pour obtenir le parallélisme. Si une méthode est liée aux E/S, exposez-la uniquement en tant qu’implémentation asynchrone.

Tâches liées au calcul

La System.Threading.Tasks.Task classe fonctionne bien pour représenter des opérations nécessitant beaucoup de ressources de calcul. Par défaut, il tire parti de la prise en charge spéciale au sein de la ThreadPool classe pour fournir une exécution efficace. Il fournit également un contrôle significatif sur le moment, l’endroit et la façon dont les calculs asynchrones s’exécutent.

Générez des tâches liées au calcul de la manière suivante :

  • Dans .NET Framework 4.5 et versions ultérieures (y compris .NET Core et .NET 5+), utilisez la méthode statique Task.Run comme raccourci vers TaskFactory.StartNew. Permet Run de lancer facilement une tâche liée au calcul qui cible le pool de threads. Cette méthode est le mécanisme préféré pour lancer une tâche liée au calcul. Utilisez StartNew directement uniquement lorsque vous souhaitez un contrôle plus précis sur la tâche.

  • Dans .NET Framework 4, utilisez la méthode TaskFactory.StartNew. Il accepte un délégué (généralement un Action<T> ou un Func<TResult>) pour s’exécuter de manière asynchrone. Si vous fournissez un Action<T> délégué, la méthode retourne un System.Threading.Tasks.Task objet qui représente l’exécution asynchrone de ce délégué. Si vous fournissez un Func<TResult> délégué, la méthode retourne un System.Threading.Tasks.Task<TResult> objet. Les surcharges de la StartNew méthode acceptent un jeton d’annulation (CancellationToken), des options de création de tâche (TaskCreationOptions) et un planificateur de tâches (TaskScheduler). Ces paramètres fournissent un contrôle précis sur la planification et l’exécution de la tâche. Une instance de fabrique qui cible le planificateur de tâches actuel est disponible en tant que propriété statique (Factory) de la Task classe. Par exemple : Task.Factory.StartNew(…).

  • Utilisez les constructeurs du Task type et de la Start méthode si vous souhaitez générer et planifier la tâche séparément. Les méthodes publiques doivent uniquement retourner des tâches qui ont déjà été démarrées.

  • Utilisez les surcharges de la méthode Task.ContinueWith. Cette méthode crée une tâche planifiée quand une autre tâche est terminée. Certaines surcharges ContinueWith acceptent un jeton d’annulation, des options de continuation et un planificateur de tâches pour un meilleur contrôle de la planification et de l’exécution de la tâche de continuation.

  • Utilisez les méthodes TaskFactory.ContinueWhenAll et TaskFactory.ContinueWhenAny. Ces méthodes créent une tâche planifiée lorsque l’ensemble ou l’un des ensembles de tâches fournis est terminé. Ces méthodes fournissent également des surcharges pour contrôler la planification et l’exécution de ces tâches.

Dans les tâches liées au calcul, le système peut empêcher l’exécution d’une tâche planifiée s’il reçoit une demande d’annulation avant de commencer à exécuter la tâche. Par conséquent, si vous fournissez un jeton d’annulation (CancellationToken objet), vous pouvez transmettre ce jeton au code asynchrone qui surveille le jeton. Vous pouvez également fournir le jeton à l’une des méthodes mentionnées précédemment, telles que StartNew ou Run afin que le Task runtime puisse également surveiller le jeton.

Par exemple, considérez une méthode asynchrone qui restitue une image. Le corps de la tâche peut interroger le jeton d’annulation afin que le code se termine tôt si une demande d’annulation arrive pendant le rendu. En outre, si la demande d’annulation arrive avant le début du rendu, vous souhaitez empêcher l’opération de rendu :

internal static Task<Bitmap> RenderAsync(ImageData data, CancellationToken cancellationToken)
{
    return Task.Run(() =>
    {
        var bmp = new Bitmap(data.Width, data.Height);
        for (int y = 0; y < data.Height; y++)
        {
            cancellationToken.ThrowIfCancellationRequested();
            for (int x = 0; x < data.Width; x++)
            {
                // render pixel [x,y] into bmp
            }
        }
        return bmp;
    }, cancellationToken);
}
Friend Function RenderAsync(data As ImageData, cancellationToken As CancellationToken) As Task(Of Bitmap)
    Return Task.Run(Function()
                        Dim bmp As New Bitmap(data.Width, data.Height)
                        For y As Integer = 0 To data.Height - 1
                            cancellationToken.ThrowIfCancellationRequested()
                            For x As Integer = 0 To data.Width - 1
                                ' render pixel [x,y] into bmp
                            Next
                        Next
                        Return bmp
                    End Function, cancellationToken)
End Function

Note

Cet exemple utilise Bitmap, ce qui nécessite le package System.Drawing.Common et est pris en charge uniquement sur Windows. Le modèle de tâche lié au calcul (à l’aide de Task.Run avec un CancellationToken) s’applique à toutes les plateformes ; remplacez-le par une bibliothèque d’imagerie multiplateforme pour les cibles non Windows.

Les tâches liées au calcul se terminent dans un Canceled état si au moins une des conditions suivantes est remplie :

  • Une demande d’annulation arrive via l’objet CancellationToken , qui est fourni en tant qu’argument de la méthode de création (par exemple, StartNew ou Run) avant la transition de la tâche vers l’état Running .

  • Une OperationCanceledException exception n’est pas gérée dans le corps d’une telle tâche. Cette exception contient le même CancellationToken qui est passé à la tâche, et ce jeton montre qu'une annulation a été demandée.

Si une autre exception n’est pas gérée dans le corps de la tâche, la tâche se termine par l’état Faulted . Toute tentative d’attente sur la tâche ou d’accès à son résultat entraîne la levée d’une exception.

Tâches liées aux E/S

Pour créer une tâche qui ne doit pas utiliser directement un thread pour l’exécution entière, utilisez le TaskCompletionSource<TResult> type. Ce type expose une Task propriété qui retourne une instance associée Task<TResult> . Vous contrôlez le cycle de vie de cette tâche à l’aide TaskCompletionSource<TResult> de méthodes telles que SetResult, , SetExceptionSetCanceledet leurs TrySet variantes.

Supposons que vous souhaitez créer une tâche qui se termine après une période spécifiée. Par exemple, vous pouvez retarder une activité dans l’interface utilisateur. La System.Threading.Timer classe offre déjà la possibilité d’appeler de façon asynchrone un délégué après une période spécifiée. En utilisant TaskCompletionSource<TResult>, vous pouvez placer un Task<TResult> devant le minuteur. Par exemple:

public static Task<DateTimeOffset> Delay(int millisecondsTimeout)
{
    TaskCompletionSource<DateTimeOffset>? tcs = null;
    Timer? timer = null;

    timer = new Timer(delegate
    {
        timer!.Dispose();
        tcs!.TrySetResult(DateTimeOffset.UtcNow);
    }, null, Timeout.Infinite, Timeout.Infinite);

    tcs = new TaskCompletionSource<DateTimeOffset>(timer);
    timer.Change(millisecondsTimeout, Timeout.Infinite);
    return tcs.Task;
}
Public Function Delay(millisecondsTimeout As Integer) As Task(Of DateTimeOffset)
    Dim tcs As TaskCompletionSource(Of DateTimeOffset) = Nothing
    Dim timer As Timer = Nothing

    timer = New Timer(Sub(obj)
                          timer.Dispose()
                          tcs.TrySetResult(DateTimeOffset.UtcNow)
                      End Sub, Nothing, Timeout.Infinite, Timeout.Infinite)

    tcs = New TaskCompletionSource(Of DateTimeOffset)(timer)
    timer.Change(millisecondsTimeout, Timeout.Infinite)
    Return tcs.Task
End Function

La Task.Delay méthode est fournie à cet effet. Vous pouvez l’utiliser à l’intérieur d’une autre méthode asynchrone, par exemple pour implémenter une boucle d’interrogation asynchrone :

public static async Task Poll(Uri url, CancellationToken cancellationToken, IProgress<bool> progress)
{
    while (true)
    {
        await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken);
        bool success = false;
        try
        {
            await DownloadStringAsync(url);
            success = true;
        }
        catch { /* ignore errors */ }
        progress.Report(success);
    }
}
Public Async Function Poll(url As Uri, cancellationToken As CancellationToken,
                           progress As IProgress(Of Boolean)) As Task
    Do While True
        Await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken)
        Dim success As Boolean = False
        Try
            Await DownloadStringAsync(url)
            success = True
        Catch
            ' ignore errors
        End Try
        progress.Report(success)
    Loop
End Function

La TaskCompletionSource<TResult> classe n’a pas d’équivalent non générique. Toutefois, Task<TResult> dérive de Task, de sorte que vous pouvez utiliser l’objet générique TaskCompletionSource<TResult> pour les méthodes liées aux E/S qui retournent simplement une tâche. Pour ce faire, utilisez une source avec un TResult factice (Boolean est un bon choix par défaut, mais si vous êtes préoccupé par l'utilisateur de la Task qui souhaite abaisser le type à un Task<TResult>, vous pouvez utiliser un type privé TResult à la place). Par exemple, la Delay méthode de l’exemple précédent retourne l’heure actuelle avec le décalage obtenu (Task<DateTimeOffset>). Si une telle valeur de résultat n’est pas nécessaire, la méthode peut plutôt être codée comme suit (notez le changement de type de retour et le changement d’argument en TrySetResult) :

public static Task<bool> DelaySimple(int millisecondsTimeout)
{
    TaskCompletionSource<bool>? tcs = null;
    Timer? timer = null;

    timer = new Timer(delegate
    {
        timer!.Dispose();
        tcs!.TrySetResult(true);
    }, null, Timeout.Infinite, Timeout.Infinite);

    tcs = new TaskCompletionSource<bool>(timer);
    timer.Change(millisecondsTimeout, Timeout.Infinite);
    return tcs.Task;
}
Public Function DelaySimple(millisecondsTimeout As Integer) As Task(Of Boolean)
    Dim tcs As TaskCompletionSource(Of Boolean) = Nothing
    Dim timer As Timer = Nothing

    timer = New Timer(Sub(obj)
                          timer.Dispose()
                          tcs.TrySetResult(True)
                      End Sub, Nothing, Timeout.Infinite, Timeout.Infinite)

    tcs = New TaskCompletionSource(Of Boolean)(timer)
    timer.Change(millisecondsTimeout, Timeout.Infinite)
    Return tcs.Task
End Function

Tâches liées au calcul mixte et liées aux E/S

Les méthodes asynchrones ne sont pas limitées aux opérations liées au calcul ou aux opérations liées aux E/S. Ils peuvent représenter un mélange des deux. En fait, vous combinez souvent plusieurs opérations asynchrones en opérations mixtes plus volumineuses. Par exemple, la RenderAsync méthode d’un exemple précédent effectue une opération gourmande en calcul pour afficher une image en fonction de certaines entrées imageData. Cela imageData peut provenir d’un service web auquel vous accédez de manière asynchrone :

public static async Task<Bitmap> DownloadDataAndRenderImageAsync(CancellationToken cancellationToken)
{
    var imageData = await DownloadImageDataAsync(cancellationToken);
    return await RenderAsync(imageData, cancellationToken);
}
Public Async Function DownloadDataAndRenderImageAsync(cancellationToken As CancellationToken) As Task(Of Bitmap)
    Dim imageData As ImageData = Await DownloadImageDataAsync(cancellationToken)
    Return Await RenderAsync(imageData, cancellationToken)
End Function

Note

Cet exemple utilise Bitmap, ce qui nécessite le package System.Drawing.Common et est pris en charge uniquement sur Windows. Le modèle de chaînage d’un téléchargement asynchrone avec une opération asynchrone liée au calcul s’applique à toutes les plateformes ; remplacez une bibliothèque d’imagerie multiplateforme pour les cibles non Windows.

Cet exemple montre également comment un jeton d’annulation unique peut être threadé via plusieurs opérations asynchrones. Pour plus d’informations, consultez la section Utilisation de l’annulation dans Consommation du modèle asynchrone basé sur les tâches.

Voir aussi