Delen via


Taakuitdrukkingen

In dit artikel wordt ondersteuning in F# beschreven voor taakexpressies, die vergelijkbaar zijn met asynchrone expressies , maar u kunt .NET-taken rechtstreeks ontwerpen. Net als asynchrone expressies voeren taakexpressies code asynchroon uit, dat wil gezegd, zonder de uitvoering van ander werk te blokkeren.

Asynchrone code wordt normaal gesproken geschreven met behulp van asynchrone expressies. Het gebruik van taakexpressies heeft de voorkeur wanneer u uitgebreid werkt met .NET-bibliotheken die .NET-taken maken of gebruiken. Taakexpressies kunnen ook de prestaties en de foutopsporingservaring verbeteren. Taakexpressies hebben echter enkele beperkingen, die verderop in het artikel worden beschreven.

Syntaxis

task { expression }

In de vorige syntaxis wordt de berekening die wordt vertegenwoordigd door expression ingesteld om te worden uitgevoerd als een .NET-taak. De taak wordt direct gestart nadat deze code is uitgevoerd en wordt uitgevoerd op de huidige thread totdat de eerste asynchrone bewerking wordt uitgevoerd (bijvoorbeeld een asynchrone slaapstand, asynchrone I/O of een andere primitieve asynchrone bewerking). Het type expressie wordt Task<'T>, waarbij 'T het type is dat door de expressie wordt geretourneerd wanneer het trefwoord return wordt gebruikt.

Binden met behulp van let!

In een taakexpressie zijn sommige expressies en bewerkingen synchroon en sommige zijn asynchroon. Wanneer u wacht op het resultaat van een asynchrone bewerking, gebruikt u in plaats van een gewone let binding let!. Het effect van let! is dat het mogelijk maakt de uitvoering van andere berekeningen of threads te laten doorgaan terwijl de berekening wordt uitgevoerd. Zodra de rechterkant van de let! binding terugkeert, hervat de rest van de taak de uitvoering.

De volgende code toont het verschil tussen let en let!. De regel code die let gebruikt, creëert gewoon een taak als een object dat u later kunt afwachten, bijvoorbeeld met behulp van task.Wait() of task.Result. De coderegel die let! gebruikt, start de taak en wacht op het resultaat.

// 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)

F# task { } -expressies kunnen wachten op de volgende soorten asynchrone bewerkingen:

return expressies

Binnen taakexpressies return expr wordt gebruikt om het resultaat van een taak te retourneren.

return! expressies

Binnen taakexpressies return! expr wordt gebruikt om het resultaat van een andere taak te retourneren. Het is gelijk aan het gebruik let! en retourneert vervolgens onmiddellijk het resultaat.

Besturing van de stroom

Taakexpressies kunnen de controleflowconstructies for .. in .. do, while .. do, try .. with .., try .. finally .., if .. then .. else, en if .. then .. bevatten. Deze kunnen op hun beurt verdere taakconstructies bevatten, met uitzondering van de with en finally handlers, die synchroon worden uitgevoerd. Als u een asynchrone try .. finally ..binding nodig hebt, gebruikt u een use binding in combinatie met een object van het type IAsyncDisposable.

bindingen voor use en use!

Binnen taakexpressies kunnen bindingen use worden gekoppeld aan waarden van het type IDisposable of IAsyncDisposable. Voor deze laatste wordt de verwijderingsopschoonbewerking asynchroon uitgevoerd.

Naast let!kunt u use! gebruiken om asynchrone bindingen uit te voeren. Het verschil tussen let! en use! is hetzelfde als het verschil tussen let en use. Voor use!wordt het object verwijderd aan het einde van het huidige bereik. Houd er rekening mee dat in F# 6 use! geen waarde mag worden geïnitialiseerd naar null, ook al use wel.

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
}

Waardetaken

Waardetaken zijn structs die worden gebruikt om toewijzingen in programmeren op basis van taken te voorkomen. Een waardetaak is een tijdelijke waarde die wordt omgezet in een echte taak met behulp van .AsTask().

Als u een waardetaak wilt maken op basis van een taakexpressie, gebruik |> ValueTask<ReturnType> of |> ValueTask. Voorbeeld:

let makeTask() =
    task { return 1 }

makeTask() |> ValueTask<int>

and! bindingen (beginnend vanaf F# 10)

Binnen taakexpressies is het mogelijk om gelijktijdig te wachten op meerdere asynchrone bewerkingen (Task<'T>, ValueTask<'T>Async<'T> enzovoort). Vergelijken:

// 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
    }

Annuleringstokens en annuleringscontroles toevoegen

In tegenstelling tot F# asynchrone expressies geven taakexpressies geen impliciet een annuleringstoken door en voeren ze geen annuleringscontroles impliciet uit. Als voor uw code een annuleringstoken is vereist, moet u het annuleringstoken opgeven als parameter. Voorbeeld:

open System.Threading

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

Als u de code correct wilt annuleren, controleert u zorgvuldig of u het annuleringstoken doorgeeft aan alle .NET-bibliotheekbewerkingen die ondersteuning bieden voor annulering. Heeft bijvoorbeeld Stream.ReadAsync meerdere overbelastingen, waarvan één een annuleringstoken accepteert. Als u deze overbelasting niet gebruikt, kan deze specifieke asynchrone leesbewerking niet worden geannuleerd.

Achtergrondtaken

.NET-taken worden standaard gepland met behulp van SynchronizationContext.Current indien aanwezig. Hierdoor kunnen taken fungeren als coöperatieve, onderling verweven agents die worden uitgevoerd op een gebruikersinterface-thread zonder de gebruikersinterface te blokkeren. Als het niet aanwezig is, worden taakcontinuaties gepland op de .NET-threadpool.

In de praktijk is het vaak wenselijk dat bibliotheekcode waarmee taken worden gegenereerd, de synchronisatiecontext negeert en in plaats daarvan altijd overschakelt naar de .NET-threadgroep, indien nodig. U kunt dit bereiken met behulp van backgroundTask { }:

backgroundTask { expression }

Een achtergrondtaak negeert elke SynchronizationContext.Current op de volgende manier: als deze is gestart op een thread met een niet-nul SynchronizationContext.Current, schakelt het over naar een achtergrondthread in de threadpool met behulp van Task.Run. Als deze is gestart op een thread met null SynchronizationContext.Current, wordt deze uitgevoerd op dezelfde thread.

Opmerking

In de praktijk betekent dit dat aanroepen ConfigureAwait(false) doorgaans niet nodig zijn in F#-taakcode. In plaats daarvan moeten taken die op de achtergrond moeten worden uitgevoerd, worden gemaakt met behulp van backgroundTask { ... }. Elke buitenste taakbinding met een achtergrondtaak wordt bij voltooiing van de achtergrondtaak opnieuw gesynchroniseerd naar de SynchronizationContext.Current.

Beperkingen van taken met betrekking tot tailcalls

In tegenstelling tot F#-asynchrone expressies bieden taakexpressies geen ondersteuning voor tailcalls. return! Wanneer return! wordt uitgevoerd, wordt de huidige taak geregistreerd als wachtend op de taak waarvan het resultaat wordt teruggegeven. Dit betekent dat recursieve functies en methoden die zijn geïmplementeerd met behulp van taakexpressies, niet-gebonden ketens van taken kunnen maken en dat deze mogelijk niet-gebonden stack of heap gebruiken. Denk bijvoorbeeld aan de volgende code:

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()

Deze coderingsstijl mag niet worden gebruikt met taakexpressies. Hiermee wordt een keten van 100000000 taken gemaakt en wordt een StackOverflowException. Als een asynchrone bewerking wordt toegevoegd aan elke lusaanroep, gebruikt de code een in wezen niet-gebonden heap. Overweeg deze code om te schakelen naar een expliciete lus, bijvoorbeeld:

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()

Als asynchrone tailcalls vereist zijn, gebruikt u een F#-asynchrone expressie die tailcalls ondersteunt. Voorbeeld:

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()

Taakimplementatie

Taken worden geïmplementeerd met behulp van hervatbare code, een nieuwe functie in F# 6. Taken worden gecompileerd in 'Hervatbare statusmachines' door de F#-compiler. Deze worden gedetailleerd beschreven in de hervatbare code RFC en in een F#-compilercommunitysessie.

Zie ook