Dela via


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:

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 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.Currentvä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.

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.

Se även