Delen via


Takenexpressies

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 van de expressie is Task<'T>, waar 'T is het type dat wordt geretourneerd door de expressie wanneer het return trefwoord 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, in plaats van een gewone let binding, gebruikt let!u . Het effect is let! dat de uitvoering kan worden voortgezet op andere berekeningen of threads terwijl de berekening wordt uitgevoerd. Nadat de rechterkant van de let! binding wordt geretourneerd, wordt de uitvoering hervat door de rest van de taak.

De volgende code toont het verschil tussen let en let!. Met de coderegel die alleen wordt gebruikt let , wordt een taak gemaakt als een object dat u later kunt wachten, bijvoorbeeld task.Wait() met task.Resultbehulp van. De coderegel die de taak gebruikt let! , wordt gestart 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.

Controlestroom

Taakexpressies kunnen de besturingsstroomconstructiesfor .. in .. do, , while .. do, try .. with .., try .. finally ..en if .. then .. elseif .. then ... 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.

use en use! bindingen

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

Daarnaast let!kunt use! u asynchrone bindingen uitvoeren. Het verschil tussen let! en use! is hetzelfde als het verschil tussen let en use. Voor use!, het object wordt 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.

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, gebruikt |> ValueTask<ReturnType> u of |> ValueTask. Voorbeeld:

let makeTask() =
    task { return 1 }

makeTask() |> ValueTask<int>

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, interleaved agents die worden uitgevoerd op een gebruikersinterfacethread zonder de gebruikersinterface te blokkeren. Als deze niet aanwezig is, worden taakvervolgingen gepland voor de .NET-threadgroep.

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 een SynchronizationContext.Current van de volgende punten: als deze is gestart op een thread met niet-null SynchronizationContext.Current, schakelt deze 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.

Notitie

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 opnieuw gesynchroniseerd naar de SynchronizationContext.Current voltooiing van de achtergrondtaak.

Beperkingen van taken met betrekking tot tailcalls

In tegenstelling tot F#-asynchrone expressies bieden taakexpressies geen ondersteuning voor tailcalls. return! Wanneer de huidige taak wordt uitgevoerd, wordt de huidige taak geregistreerd als wachtend op de taak waarvan het resultaat wordt geretourneerd. 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