Partager via


Expressions de tâche

Cet article décrit la prise en charge de F# pour les expressions de tâche, qui sont similaires aux expressions asynchrones , mais vous permettent de créer directement des tâches .NET. Comme les expressions asynchrones, les expressions de tâche exécutent du code de manière asynchrone, autrement dit, 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érée lors de l’interopérabilité étendue 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 présentent 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 est démarrée 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 veille asynchrone, des E/S asynchrones 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 return mot clé est utilisé.

Liaison à l’aide de let !

Dans une expression de tâche, certaines expressions et opérations sont synchrones, et certaines 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 est de permettre à l’exécution de let! 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 let! liaison est retourné, le reste de la tâche reprend l’exécution.

Le code suivant montre la différence entre let et let!. Ligne de code qui utilise let simplement crée une tâche en tant qu’objet que vous pouvez attendre ultérieurement à l’aide, 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 F# task { } peuvent attendre les types d’opérations asynchrones suivants :

Expressions return

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

Expressions return!

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

Flux de contrôle

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

use et use! liaisons

Dans les expressions de tâche, use les liaisons peuvent être liées à des valeurs de type IDisposable ou IAsyncDisposable. Pour ce dernier, l’opération de nettoyage de la 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 la même que la différence entre let et use. Pour use!, l’objet est supprimé à la fermeture de l’étendue actuelle. Notez que dans F# 6, use! n’autorise pas l’initialisation d’une valeur à null, même si use elle le fait.

open System
open System.IO
open System.Security.Cryptography
task {
    // use IDisposable
    use httpClient = new Net.Http.HttpClient()
    // use! Task<IDisposable>
    use! exampleDomain = httpClient.GetAsync "https://example.com/data.enc"
   
    // use IDisposable
    use aes = Aes.Create()
    aes.KeySize <- 256
    aes.GenerateIV()
    aes.GenerateKey()
    // do! Task
    do! File.WriteAllTextAsync("key.iv.txt", $"Key: {Convert.ToBase64String aes.Key}\nIV: {Convert.ToBase64String aes.IV}")

    // use IAsyncDisposable
    use outputStream = File.Create "secret.enc"
    // use IDisposable
    use encryptor = aes.CreateEncryptor()
    // use IAsyncDisposable
    use cryptoStream = new CryptoStream(outputStream, encryptor, CryptoStreamMode.Write)
    // do! Task
    do! exampleDomain.Content.CopyToAsync cryptoStream
}

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 une tâche réelle à l’aide .AsTask()de .

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>

and! liaisons (à partir de F# 10)

Dans les expressions de tâche, il est possible d’attendre simultanément plusieurs opérations asynchrones (Task<'T>, ValueTask<'T>Async<'T> etc.). Comparer:

// We'll wait for x to resolve and then for y to resolve. Overall execution time is sum of two execution times.
let getResultsSequentially() =
    task {
        let! x = getX()
        let! y = getY()
        return x, y
    }

// x and y will be awaited concurrently. Overall execution time is the time of the slowest operation.
let getResultsConcurrently() =
    task {
        let! x = getX()
        and! y = getY()
        return x, y
    }

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 il existe 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 peut pas être annulée.

Tâches en arrière-plan

Par défaut, les tâches .NET sont planifiées à l’aide SynchronizationContext.Current du 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. S’il n’est pas présent, 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 la 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. Vous pouvez effectuer cette opération à l’aide backgroundTask { }de :

backgroundTask { expression }

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

Remarque

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 backgroundTask { ... }de . Toute liaison de tâche externe à une tâche en arrière-plan est resynchronisée à l’achèvement SynchronizationContext.Current de la tâche en arrière-plan.

Limitations des tâches concernant les appels de queue

Contrairement aux expressions asynchrones F#, les expressions de tâche ne prennent pas en charge les tailcalls. Autrement dit, lorsqu’elle return! est exécutée, la tâche actuelle est inscrite en attente de la tâche dont le résultat est retourné. Cela signifie que les fonctions et méthodes récursives implémentées à l’aide d’expressions de tâche peuvent créer des chaînes de tâches sans limite, 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ée une chaîne de 1000000 tâches et provoque un StackOverflowException. Si une opération asynchrone est ajoutée sur chaque appel de boucle, le code utilise un tas essentiellement indépendant. 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 requis, 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 de tâche

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

Voir aussi