Uppgiftsuttryck
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ökning. 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 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 let!
du i stället för en vanlig let
bindning . Effekten av let!
är att aktivera körning för att fortsätta på andra beräkningar eller trådar när 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 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 .. else
och if .. then ..
. Dessa kan i sin tur omfatta ytterligare aktivitetskonstruktioner, förutom with
hanterare och finally
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!
bindningar
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
. För use!
tas objektet bort i slutet av det aktuella omfånget. Observera att i F# 6 use!
tillåter inte att ett värde initieras till null, även om use
det gör det.
Värdeuppgifter
Värdeuppgifter är structs som används för att undvika allokeringar i aktivitetsbaserad programmering. En värdeaktivitet är ett tillfälliga värde som omvandlas till en verklig aktivitet med hjälp .AsTask()
av .
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>
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 om SynchronizationContext.Current de finns. Detta gör att uppgifter kan fungera som samarbetsinriktade, interleaved agenter som körs på en användargränssnittstråd utan att blockera användargränssnittet. 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 bemärkelse: 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.Current
körs den på samma tråd.
Kommentar
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 yttre aktivitetsbindningar till en bakgrundsaktivitet synkroniseras om till när SynchronizationContext.Current
bakgrundsaktiviteten har slutförts.
Begränsningar för uppgifter som rör tailcalls
Till skillnad från Asynkrona F#-uttryck stöder aktivitetsuttryck inte tailcalls. Det innebär att när return!
den körs registreras den aktuella aktiviteten som väntar på den aktivitet vars resultat returneras. Det innebär att rekursiva funktioner och metoder som implementeras med hjälp av uppgiftsuttryck kan skapa obundna aktivitetskedjor, och dessa kan använda obundna staplar eller heap. 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 i varje loopanrop använder koden en i stort sett obundna heap. Ö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 F#-asynkront uttryck, som stöder 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()
Uppgiftsimplementering
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 återförbrukningsbar kod och i en F#-kompilatorcommunitysession.