Anteckning
Åtkomst till den här sidan kräver auktorisering. Du kan prova att logga in eller ändra kataloger.
Åtkomst till den här sidan kräver auktorisering. Du kan prova att ändra kataloger.
I den här artikeln beskrivs stöd i F# för uppgiftsuttryck, som liknar asynkrona uttryck , men som gör att du kan skapa .NET-uppgifter direkt. Precis som asynkrona uttryck kör uppgiftsuttryck kod asynkront, dvs. utan att blockera körning av annat arbete.
Asynkron kod skapas normalt med hjälp av asynkrona uttryck. Användning av uppgiftsuttryck är att föredra när du samverkar i stor utsträckning med .NET-bibliotek som skapar eller använder .NET-uppgifter. Uppgiftsuttryck kan också förbättra prestanda och felsökningsupplevelse. Uppgiftsuttryck har dock vissa begränsningar, som beskrivs senare i artikeln.
Syntax
task { expression }
I den tidigare syntaxen konfigureras beräkningen som representeras av expression för att köras som en .NET-uppgift. Uppgiften startas omedelbart efter att den här koden har körts och körs på den aktuella tråden tills den första asynkrona åtgärden utförs (till exempel en asynkron viloläge, asynkron I/O eller någon annan primitiv asynkron åtgärd). Uttryckets typ är Task<'T>, där 'T är den typ som returneras av uttrycket när nyckelordet return används.
Bindning med hjälp av let
I ett aktivitetsuttryck är vissa uttryck och åtgärder synkrona och vissa är asynkrona. När du väntar på resultatet av en asynkron åtgärd använder letdu i stället för en vanlig let! bindning . Effekten av let! är att möjliggöra fortsatt körning av andra beräkningar eller trådar medan beräkningen utförs. När den högra sidan av bindningen let! har returnerats återupptas körningen av resten av aktiviteten.
Följande kod visar skillnaden mellan let och let!. Kodraden som använder let skapar bara en uppgift som ett objekt som du kan vänta på senare med hjälp av till exempel task.Wait() eller task.Result. Kodraden som använder let! startar aktiviteten och väntar på resultatet.
// 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 { } uttryck kan vänta på följande typer av asynkrona åtgärder:
- .NET-uppgifter, Task<TResult> och de icke-generiska Task.
- .NET-värdeuppgifter ValueTask<TResult> och icke-generiska ValueTask.
- F#-asynkrona
Async<T>beräkningar. - Alla objekt som följer mönstret "GetAwaiter" som anges i F# RFC FS-1097.
return uttryck
I aktivitetsuttryck return expr används för att returnera resultatet av en aktivitet.
return! uttryck
I aktivitetsuttryck return! expr används för att returnera resultatet av en annan aktivitet. Det motsvarar att använda let! och sedan omedelbart returnera resultatet.
Kontrollflöde
Uppgiftsuttryck kan innehålla kontrollflödeskonstruktionerna for .. in .. do, while .. do, try .. with .., try .. finally .., , if .. then .. elseoch if .. then ... Dessa kan i sin tur omfatta ytterligare aktivitetskonstruktioner, förutom with- och finally-hanterarna som körs synkront. Om du behöver en asynkron try .. finally ..använder du en use bindning i kombination med ett objekt av typen IAsyncDisposable.
use och use! kopplingar
I aktivitetsuttryck use kan bindningar binda till värden av typen IDisposable eller IAsyncDisposable. För det senare körs rensningsåtgärden asynkront.
Förutom let!kan du använda use! för att utföra asynkrona bindningar. Skillnaden mellan let! och use! är densamma som skillnaden mellan let och use. Objektet use! tas bort i slutet av den aktuella räckvidden. Observera att i F# 6 use! tillåter inte att ett värde initieras till null, även om use det gör det.
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
}
Värdeuppgifter
Värdeuppgiftsstrukturer är strukturer som används för att undvika allokeringar i uppgiftsbaserad programmering. En värdeaktivitet är ett tillfälligt värde som omvandlas till en verklig uppgift med hjälp av .AsTask().
Om du vill skapa en värdeaktivitet från ett aktivitetsuttryck använder du |> ValueTask<ReturnType> eller |> ValueTask. Till exempel:
let makeTask() =
task { return 1 }
makeTask() |> ValueTask<int>
and! bindningar (från F# 10)
I aktivitetsuttryck är det möjligt att samtidigt vänta på flera asynkrona åtgärder (Task<'T>, ValueTask<'T>osv Async<'T> .). Jämföra:
// 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
}
Lägga till annulleringstoken och annulleringskontroller
Till skillnad från Asynkrona F#-uttryck skickar aktivitetsuttryck inte implicit en annulleringstoken och utför inte implicit annulleringskontroller. Om koden kräver en annulleringstoken bör du ange annulleringstoken som en parameter. Till exempel:
open System.Threading
let someTaskCode (cancellationToken: CancellationToken) =
task {
cancellationToken.ThrowIfCancellationRequested()
printfn $"continuing..."
}
Om du vill att koden ska kunna avbrytas korrekt kontrollerar du noggrant att du skickar annulleringstoken till alla .NET-biblioteksåtgärder som stöder annullering. Till exempel Stream.ReadAsync har flera överlagringar, varav en accepterar en annulleringstoken. Om du inte använder den här överlagringen kan den specifika asynkrona läsåtgärden inte avbrytas.
Bakgrundsaktiviteter
Som standard schemaläggs .NET-aktiviteter med SynchronizationContext.Current om det finns. Detta gör att uppgifter kan fungera som samarbetsvilliga, omväxlande agenter som körs på en användargränssnittstråd utan att blockera UI:t. Om de inte finns schemaläggs aktivitetsfortsättningar till .NET-trådpoolen.
I praktiken är det ofta önskvärt att bibliotekskod som genererar uppgifter ignorerar synkroniseringskontexten och i stället alltid växlar till .NET-trådpoolen om det behövs. Du kan uppnå detta med hjälp av backgroundTask { }:
backgroundTask { expression }
En bakgrundsaktivitet ignorerar alla SynchronizationContext.Current i följande mening: om den startas på en tråd med icke-null SynchronizationContext.Current, växlar den till en bakgrundstråd i trådpoolen med Task.Run. Om den startas på en tråd med null SynchronizationContext.Currentkörs den på samma tråd.
Anmärkning
I praktiken innebär det att anrop till ConfigureAwait(false) vanligtvis inte behövs i F#-aktivitetskoden. I stället bör uppgifter som är avsedda att köras i bakgrunden redigeras med hjälp av backgroundTask { ... }. Alla externa uppgiftsbindningar till en bakgrundsaktivitet kommer att åter synkroniseras till SynchronizationContext.Current när bakgrundsaktiviteten har slutförts.
Begränsningar för uppdrag som rör tailcalls
Till skillnad från Asynkrona F#-uttryck stöder aktivitetsuttryck inte tailcalls. Det innebär att när return! körs, registreras den aktuella uppgiften som väntar på den uppgift vars resultat returneras. Det innebär att rekursiva funktioner och metoder som implementeras med hjälp av uppgiftsuttryck kan skapa obegränsade uppgiftskedjor, och dessa kan använda obegränsade stackar eller heapar. Tänk till exempel på följande kod:
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()
Det här kodningsformatet ska inte användas med uppgiftsuttryck– det skapar en kedja med 10000000 aktiviteter och orsakar en StackOverflowException. Om en asynkron åtgärd läggs till vid varje loop-iteration använder koden i stort sett obegränsat minne. Överväg att växla den här koden till att använda en explicit loop, till exempel:
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()
Om asynkrona tailcalls krävs använder du ett asynkront F#-uttryck, som stödjer tailcalls. Till exempel:
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()
Genomförande av uppgift
Uppgifter implementeras med hjälp av Återanvändbar kod, en ny funktion i F# 6. Uppgifter kompileras till "Resumable State Machines" av F#-kompilatorn. Dessa beskrivs i detalj i RFC för återupptagbar kod och i en F# kompilator community session.