Tevékenységkifejezések

Ez a cikk az F#-ban nyújtott támogatást ismerteti az aszinkron kifejezésekhez hasonló feladatkifejezésekhez , de lehetővé teszi a .NET-feladatok közvetlen készítését. Az aszinkron kifejezésekhez hasonlóan a tevékenységkifejezések is aszinkron módon hajtják végre a kódot, azaz anélkül, hogy más munka végrehajtását blokkolják.

Az aszinkron kód létrehozása általában aszinkron kifejezések használatával történik. A feladatkifejezések használata akkor ajánlott, ha a .NET-feladatokat létrehozó vagy használó .NET-kódtárakkal együttműködik. A feladatkifejezések a teljesítményt és a hibakeresési élményt is javíthatják. A tevékenységkifejezések azonban bizonyos korlátozásokkal járnak, amelyeket a cikk későbbi részében ismertetünk.

Szemantika

task { expression }

Az előző szintaxisban az általa expression képviselt számítás .NET-feladatként való futtatásra van beállítva. A feladat közvetlenül a kód végrehajtása után indul el, és az aktuális szálon fut az első aszinkron művelet végrehajtásáig (például aszinkron alvó állapot, aszinkron I/O vagy más primitív aszinkron művelet). A kifejezés Task<'T>típusa az, ahol 'T a kulcsszó használatakor a kifejezés return által visszaadott típus található.

Kötés a let használatával!

A feladatkifejezésekben egyes kifejezések és műveletek szinkronok, mások pedig aszinkronok. Amikor egy aszinkron művelet eredményére vár, a szokásos let kötés let!helyett a . Ennek az a hatása let! , hogy lehetővé teszi a végrehajtás folytatását más számításokon vagy szálakon a számítás végrehajtása során. Miután a kötés jobb oldala let! visszatér, a tevékenység többi része folytatja a végrehajtást.

Az alábbi kód a kettő és leta kettő közötti let! különbséget mutatja be. A kódsor, amely csak let egy feladatot hoz létre objektumként, amelyet később is várhat, például task.Wait()task.Resulta . A feladatot használó let! kódsor elindítja a feladatot, és várja annak eredményét.

// 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)

Az F# task { } -kifejezések az alábbi aszinkron műveletekre várnak:

return kifejezések

A tevékenységkifejezéseken return expr belül a rendszer egy tevékenység eredményét adja vissza.

return! kifejezések

A feladatkifejezéseken return! expr belül egy másik tevékenység eredményét adja vissza. Ez egyenértékű az eredmény használatával let! , majd azonnal visszaadásával.

Vezérlési folyamat

A tevékenységkifejezések tartalmazhatják a vezérlőfolyamat-szerkezeteket for .. in .. do, while .. do, , try .. with ..try .. finally .., if .. then .. elseés if .. then ... Ezek további feladatszerkezeteket is tartalmazhatnak, kivéve azokat a with kezelőket finally , amelyek szinkron módon hajtanak végre. Ha aszinkronra try .. finally ..van szüksége, használjon kötést use egy típusú IAsyncDisposableobjektummal kombinálva.

use és use! kötések

A tevékenységkifejezéseken use belül a kötések a típus IDisposableIAsyncDisposablevagy a . Az utóbbi esetében a rendszer aszinkron módon hajtja végre az ártalmatlanítási tisztítási műveletet.

let!Emellett aszinkron use! kötéseket is végrehajthat. A különbség let!use! és ugyanaz, mint a különbség let és usea . Ehhez use!az objektum az aktuális hatókör végén lesz megsemmisítve. Vegye figyelembe, hogy az F# 6-ban nem engedélyezi az use! érték null értékűre való inicializálását, annak ellenére use , hogy igen.

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
}

Értékfeladatok

Az értékfeladatok olyan szerkezetek, amelyek a tevékenységalapú programozásban való foglalások elkerülésére szolgálnak. Az értékfeladat egy rövid élettartamú érték, amely valós tevékenységgé alakul a használatával .AsTask().

Ha értékfeladatot szeretne létrehozni egy tevékenységkifejezésből, használja |> ValueTask<ReturnType> vagy |> ValueTask. Például:

let makeTask() =
    task { return 1 }

makeTask() |> ValueTask<int>

and! kötések (F# 10-től kezdődően)

A feladatkifejezéseken belül egyidejűleg több aszinkron műveletre (Task<'T>stbValueTask<'T>Async<'T>.) is lehet várni. Összehasonlít:

// 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
    }

Lemondási jogkivonatok és lemondási ellenőrzések hozzáadása

Az F# aszinkron kifejezésektől eltérően a tevékenységkifejezések nem adják át implicit módon a lemondási jogkivonatot, és nem hajtanak végre implicit módon lemondási ellenőrzéseket. Ha a kódhoz lemondási jogkivonat szükséges, paraméterként meg kell adnia a lemondási jogkivonatot. Például:

open System.Threading

let someTaskCode (cancellationToken: CancellationToken) =
    task {
        cancellationToken.ThrowIfCancellationRequested()
        printfn $"continuing..."
    }

Ha helyesen szeretné visszavonhatóvá tenni a kódot, gondosan ellenőrizze, hogy a lemondási jogkivonatot átadja-e minden olyan .NET-kódtár-műveletnek, amely támogatja a lemondást. Például Stream.ReadAsync több túlterheléssel rendelkezik, amelyek közül az egyik elfogad egy lemondási jogkivonatot. Ha nem használja ezt a túlterhelést, az adott aszinkron olvasási művelet nem lesz megszakítható.

Kötött egyidejűség és eredményfolyamatok

Az FSharp.Core 11 biztosítja Task.parallelLimit, hogy egyszerre hány művelet induljon el. A már megkezdett feladatok helyett adja át a lemondási jogkivonatokat elfogadó gyárakat:

open System.IO
open System.Threading

let readFiles (paths: seq<string>) (ct: CancellationToken) =
    paths
    |> Seq.map (fun path ->
        fun token -> File.ReadAllTextAsync(path, token))
    |> Task.parallelLimit 4 ct

Az eredménytömb megőrzi a bemeneti sorrendet. A hiba a testvérmunka törlését kéri, és minden műveletnek meg kell tartania a jogkivonatát. Task.sequential gyárakat működtet egyenként; Task.parallelDoLimit tömb Task.sequentialDo helyett adja vissza unit .

A , Task.bind, és Task.catch eredményfolyamatok, valamint az Async/ValueTask segédjeinek megfelelő folyamatokért Task.maplásd az Async és a tevékenységfüggvényeket.

Háttérfeladatok

Alapértelmezés szerint a .NET-tevékenységek ütemezése SynchronizationContext.Current a jelen esetben történik. Ez lehetővé teszi, hogy a feladatok együttműködő, interleaved ügynökökként szolgáljanak, és a felhasználói felület szálán hajtanak végre anélkül, hogy blokkolnák a felhasználói felületet. Ha nincs jelen, a tevékenység folytatását a rendszer a .NET-szálkészletbe ütemezi.

A gyakorlatban gyakran kívánatos, hogy a feladatokat generáló kódtárkód figyelmen kívül hagyja a szinkronizálási környezetet, és szükség esetén mindig a .NET-szálkészletre vált. Ezt a következővel backgroundTask { }érheti el:

backgroundTask { expression }

A háttérfeladatok figyelmen kívül hagyják a következő értelemben vetteket SynchronizationContext.Current : ha nem null SynchronizationContext.Currentértékű szálon indul el, akkor a szálkészlet háttérszálára vált a használatával Task.Run. Ha null értékű SynchronizationContext.Currentszálon indult el, az ugyanazon a szálon fut.

Megjegyzés:

A gyakorlatban ez azt jelenti, hogy az F# feladatkódban általában nincs szükség a hívásokra ConfigureAwait(false) . Ehelyett a háttérben futtatandó feladatokat a következővel backgroundTask { ... }kell létrehozni: . A háttértevékenységhez tartozó külső tevékenységkötések újraszinkronizálva lesznek a SynchronizationContext.Current háttértevékenység befejezésekor.

A tailcallokkal kapcsolatos feladatok korlátozásai

Az F# aszinkron kifejezésektől eltérően a tevékenységkifejezések nem támogatják a tailcalls használatát. Ez azt jelenti, hogy a végrehajtáskor return! a rendszer az aktuális tevékenységet a visszaadott feladatra várva regisztrálja. Ez azt jelenti, hogy a feladatkifejezésekkel implementált rekurzív függvények és metódusok kötetlen tevékenységláncokat hozhatnak létre, és ezek kötetlen vermet vagy halomhalomot is használhatnak. Vegyük például a következő kódot:

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()

Ezt a kódolási stílust nem szabad tevékenységkifejezésekkel használni– ez 100000000 tevékenységláncot hoz létre, és egy StackOverflowException. Ha minden ciklushíváshoz aszinkron művelet van hozzáadva, a kód lényegében kötetlen halomra fog támaszkodni. Fontolja meg a kód explicit hurok használatára való váltását, például:

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()

Ha aszinkron tailcallokra van szükség, használjon F# aszinkron kifejezést, amely támogatja a tailcallokat. Például:

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()

Feladat implementálása

A feladatok az F# 6 új funkciójával, a Resumable Code-tal implementálhatók. A feladatokat az F#-fordító az "Újraművelhető állapotgépek" részre fordítja. Ezek részletes leírását az Újracsomagoló kód RFC-jében és egy F#-fordítói közösségi munkamenetben ismertetjük.

Futtatókörnyezeti aszinkron kísérletek

A készítők kísérletezhetnek .NET futtatókörnyezeti aszinkronnal. A fordító által létrehozott állapotgép általában a várakozás után a folytatáshoz szükséges állapotot tárolja. A futtatókörnyezet aszinkron használatával a futtatókörnyezet szünetelteti és folytatja a speciálisan megjelölt metódust.

Megjegyzés:

Ez a prototípus egy F# 11.2 funkciót használ. A .NET 11.0.100 SDK-ban engedélyezze <LangVersion>preview</LangVersion> a futtatókörnyezet-aszinkron támogatást tartalmazó fordítót. Emellett futtatókörnyezetre és referencia-szerelvényekre is szükség van.MethodImplOptions.AsyncSystem.Runtime.CompilerServices.AsyncHelpers.Await A referenciaszerelvények fordításkor írják le az API-kat; a futó .NET futtatókörnyezetnek végre kell hajtania a szüneteltetési/folytatási viselkedést.

A példa ellenőrzött összetevőkombinációja a következő:

Komponens Verzió vagy követelmény
Fordítóprogram Az FSharp.Compiler.Service 43.13.101-rc2.26473.112által biztosított F# 11 fordító.
FSharp.Core Package 11.0.101-rc2.26473.112, using lib/net10.0/FSharp.Core.dll. Ez az összeállítás biztosítja a szükséges elemetStateMachineHelpers.__runtimeAsyncReturn; az és netstandard2.1 a netstandard2.0 buildek nem.
Project cél net11.0.
Futtatókörnyezet és referenciaszerelvények 11.0.0-rc.1.26425.128.NET .

Ezek a tesztelt összetevők nem egyetlen SDK-disztribúció. .NET 10.0.12 elérhetővé teszi a referencia API-kat, de a szüneteltetés/folytatás ellenőrzése a futtatókörnyezetben TypeLoadExceptionmeghiúsul. Használja a fenti ellenőrzött futtatókörnyezet/referencia kombinációt; sem az előzetes verziójú mód, sem az API-jelenlét nem biztosít futtatókörnyezeti támogatást.

A következő híváswebhely egy alkalmazás által definiált szerkesztővel aszinkron módon olvassa be a fájlokat:

let readManifest (path: string) =
    runtimeTask {
        let! text = System.IO.File.ReadAllTextAsync path
        return text.Length
    }

A szerkesztő összeköti a számítási kifejezést a futtatókörnyezet-aszinkron visszatérési belsővel, és várja a segítőt. Ez nem egy új beépített task építő:

open System.Threading.Tasks
open System.Runtime.CompilerServices
open Microsoft.FSharp.Core.CompilerServices

type RuntimeTaskBuilder() =
    member inline _.Delay([<InlineIfLambda>] code: unit -> 'T) = code
    member inline _.Run([<InlineIfLambda>] code: unit -> 'T) : Task<'T> =
        StateMachineHelpers.__runtimeAsyncReturn (code())
    member inline _.Return(value: 'T) = value
    member inline _.Bind(work: Task<'T>, [<InlineIfLambda>] next: 'T -> 'U) =
        next (AsyncHelpers.Await work)

let runtimeTask = RuntimeTaskBuilder()

__runtimeAsyncReturn A runtime async beágyazási metódusát jelöli. Ezen a módszeren belül felfüggesztheti a befejezetlen munkát, AsyncHelpers.Await és visszaadhatja az irányítást a hívónak, majd folytathatja az eredményt. Ez nem blokkoló .Result hívás.

A fordító ellenőrzi a támogatott felfüggesztési mintákat; a byrefs nem maradhat élőben egy felfüggesztésen keresztül. A példa csak egy Bind túlterhelést határoz megReturnDelayRun, és nem a megszakítással, hurkokkal vagy aszinkron ártalmatlanítással rendelkező gyártót.

Engedélyezze az F# 11.2 funkciót, amelyben a beágyazott szerkesztő definiálva van és ki van bontva. A már lefordított readManifest API-k hívóinak nincs rá szükségük. A szokásos task kifejezések megtartják a megvalósításukat. Csak futásidejű aszinkron metódusokat használjon AsyncHelpers.Await ; lásd a futtatókörnyezet aszinkron specifikációját.

A teljes mintaprojekt tartalmaz egy konzolbeléptetési pontot. Futtassa dotnet run -- manifest.txt a könyvtárában a kompatibilis eszközkészlettel és egy olvasható fájllal. A pontosan manifest, záró új vonal nélkül tartalmazó fájl létrehozza a következőt 8: . A konzol belépési pont blokkja a befejezéshez; readManifest maga ad Task<int>vissza .

Lásd még