Notitie
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen u aan te melden of de directory te wijzigen.
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen de mappen te wijzigen.
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:
- .NET-taken Task<TResult> en de niet-generieke Task.
- .NET-waardetaken ValueTask<TResult> en de niet-generieke ValueTask.
- F# asynchrone berekeningen
Async<T>
. - Elk object volgens het patroon 'GetAwaiter' dat is opgegeven in F# RFC FS-1097.
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.