Expressions des tâches

Cet article décrit la prise en charge dans F# des expressions de tâche, qui sont similaires aux expressions asynchrones, mais qui vous permettent de créer des tâches .NET directement. Comme les expressions asynchrones, les expressions de tâche exécutent du code de manière asynchrone, c’est-à-dire sans bloquer l’exécution d’autres travaux.

Le code asynchrone est normalement créé à l’aide d’expressions asynchrones. L’utilisation d’expressions de tâche est préférable lors de l’interopérabilité intensive avec les bibliothèques .NET qui créent ou consomment des tâches .NET. Les expressions de tâche peuvent également améliorer les performances et l’expérience de débogage. Toutefois, les expressions de tâche sont accompagnées de certaines limitations, qui sont décrites plus loin dans l’article.

Syntaxe

task { expression }

Dans la syntaxe précédente, le calcul représenté par expression est configuré pour s’exécuter en tant que tâche .NET. La tâche démarre immédiatement après l’exécution de ce code et s’exécute sur le thread actuel jusqu’à ce que sa première opération asynchrone soit effectuée (par exemple, une mise en veille asynchrone, des E/S asynchrones ou une autre opération asynchrone primitive). Le type de l’expression est Task<'T>, où 'T est le type retourné par l’expression lorsque le mot clé return est utilisé.

Liaison à l’aide de let!

Dans une expression de tâche, certaines expressions et opérations sont synchrones, et d’autres sont asynchrones. Lorsque vous attendez le résultat d’une opération asynchrone, au lieu d’une liaison ordinaire let, vous utilisez let!. L’effet de let! est de permettre à l’exécution de continuer sur d’autres calculs ou threads à mesure que le calcul est en cours d’exécution. Une fois que le côté droit de la liaison let! est retourné, le reste de la tâche reprend l’exécution.

Le code suivant montre la différence entre let et let!. La ligne de code qui utilise let crée simplement une tâche en tant qu’objet que vous pouvez attendre ultérieurement en utilisant, par exemple, task.Wait() ou task.Result. La ligne de code qui utilise let! démarre la tâche et attend son résultat.

// let just stores the result as a task.
let (result1 : Task<int>) = stream.ReadAsync(buffer, offset, count, cancellationToken)
// let! completes the asynchronous operation and returns the data.
let! (result2 : int)  = stream.ReadAsync(buffer, offset, count, cancellationToken)

Les expressions task { } F# peuvent attendre les types d’opérations asynchrones suivants :

Expressions return

Dans les expressions de tâche, return expr est utilisé pour retourner le résultat d’une tâche.

Expressions return!

Dans les expressions de tâche, return! expr est utilisé pour retourner le résultat d’une autre tâche. Cela équivaut à utiliser let!, puis à retourner immédiatement le résultat.

Flux de contrôle

Les expressions de tâche peuvent inclure les constructions de flux de contrôle for .. in .. do, while .. do, try .. with .., try .. finally .., if .. then .. else et if .. then ... Elles peuvent à leur tour inclure d’autres constructions de tâches, à l’exception des gestionnaires with et finally, qui s’exécutent de manière synchrone. Si vous avez besoin d’un try .. finally .. asynchrone, utilisez une liaison use en combinaison avec un objet de type IAsyncDisposable.

Liaisons use et use!

Dans les expressions de tâche, les liaisons use peuvent être liées à des valeurs de type IDisposable ou IAsyncDisposable. Pour ce dernier, l’opération de nettoyage de suppression est exécutée de manière asynchrone.

En plus de let!, vous pouvez utiliser use! pour effectuer des liaisons asynchrones. La différence entre let! et use! est identique à la différence entre let et use. Pour use!, l’objet est supprimé à la fin de l’étendue actuelle. Notez que, dans F# 6, use! n’autorise pas l’initialisation d’une valeur à null, même si use le fait.

Tâches de valeur

Les tâches de valeur sont des structs utilisés pour éviter les allocations dans la programmation basée sur les tâches. Une tâche de valeur est une valeur éphémère qui est transformée en tâche réelle à l’aide de .AsTask().

Pour créer une tâche de valeur à partir d’une expression de tâche, utilisez |> ValueTask<ReturnType> ou |> ValueTask. Par exemple :

let makeTask() =
    task { return 1 }

makeTask() |> ValueTask<int>

Ajout de jetons d’annulation et de vérifications d’annulation

Contrairement aux expressions asynchrones F#, les expressions de tâche ne passent pas implicitement un jeton d’annulation et n’effectuent pas implicitement de vérifications d’annulation. Si votre code nécessite un jeton d’annulation, vous devez spécifier le jeton d’annulation en tant que paramètre. Par exemple :

open System.Threading

let someTaskCode (cancellationToken: CancellationToken) =
    task {
        cancellationToken.ThrowIfCancellationRequested()
        printfn $"continuing..."
    }

Si vous envisagez de rendre votre code annulable correctement, vérifiez soigneusement que vous transmettez le jeton d’annulation à toutes les opérations de bibliothèque .NET qui prennent en charge l’annulation. Par exemple, Stream.ReadAsync a plusieurs surcharges, dont l’une accepte un jeton d’annulation. Si vous n’utilisez pas cette surcharge, cette opération de lecture asynchrone spécifique ne sera pas annulable.

Tâches en arrière-plan

Par défaut, les tâches .NET sont planifiées à l’aide de SynchronizationContext.Current, le cas échéant. Cela permet aux tâches de servir d’agents coopératifs et entrelacés s’exécutant sur un thread d’interface utilisateur sans bloquer l’interface utilisateur. Si elles ne sont pas présentes, les continuations de tâches sont planifiées dans le pool de threads .NET.

Dans la pratique, il est souvent souhaitable que le code de bibliothèque qui génère des tâches ignore le contexte de synchronisation et bascule toujours vers le pool de threads .NET, si nécessaire. Pour ce faire, vous pouvez utiliser backgroundTask { } :

backgroundTask { expression }

Une tâche en arrière-plan ignore les SynchronizationContext.Current dans le sens suivant : si elle a démarré sur un thread avec des SynchronizationContext.Current non null, elle bascule vers un thread en arrière-plan dans le pool de threads à l’aide de Task.Run. Si elle a démarré sur un thread avec un SynchronizationContext.Current null, elle s’exécute sur ce même thread.

Notes

Dans la pratique, cela signifie que les appels à ConfigureAwait(false) ne sont généralement pas nécessaires dans le code de tâche F#. Au lieu de cela, les tâches destinées à s’exécuter en arrière-plan doivent être créées à l’aide de backgroundTask { ... }. Toute liaison de tâche externe à une tâche en arrière-plan est resynchronisée sur SynchronizationContext.Current à l’achèvement de la tâche en arrière-plan.

Limitations des tâches concernant les tailcalls

Contrairement aux expressions asynchrones F#, les expressions de tâche ne prennent pas en charge les tailcalls. Autrement dit, quand vous exécutez return!, la tâche actuelle est inscrite comme étant en attente de la tâche dont le résultat est retourné. Cela signifie que les fonctions récursives et les méthodes implémentées à l’aide d’expressions de tâche peuvent créer des chaînes de tâches non liées, et celles-ci peuvent utiliser une pile ou un tas sans limite. Considérons par exemple le code suivant :

let rec taskLoopBad (count: int) : Task<string> =
    task {
        if count = 0 then
            return "done!"
        else
            printfn $"looping..., count = {count}"
            return! taskLoopBad (count-1)
    }

let t = taskLoopBad 10000000
t.Wait()

Ce style de codage ne doit pas être utilisé avec des expressions de tâche : il créerait une chaîne de 10 000 000 tâches et provoquerait une StackOverflowException. Si une opération asynchrone est ajoutée à chaque appel de boucle, le code utilise un tas essentiellement non lié. Envisagez de changer ce code pour utiliser une boucle explicite, par exemple :

let taskLoopGood (count: int) : Task<string> =
    task {
        for i in count .. 1 do
            printfn $"looping... count = {count}"
        return "done!"
    }

let t = taskLoopGood 10000000
t.Wait()

Si des tailcalls asynchrones sont nécessaires, utilisez une expression asynchrone F#, qui prend en charge les tailcalls. Par exemple :

let rec asyncLoopGood (count: int) =
    async {
        if count = 0 then
            return "done!"
        else
            printfn $"looping..., count = {count}"
            return! asyncLoopGood (count-1)
    }

let t = asyncLoopGood 1000000 |> Async.StartAsTask
t.Wait()

Implémentation des tâches

Les tâches sont implémentées à l’aide de code pouvant être repris, une nouvelle fonctionnalité de F# 6. Les tâches sont compilées en « Ordinateurs d’état pouvant être repris » par le compilateur F#. Ceux-ci sont décrits en détail dans le RFC du code pouvant être repris et dans une session de la communauté pour le compilateur F#.

Voir aussi