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.

Syntax

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 let!a 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.

Átvitelvezérlés

A tevékenységkifejezések tartalmazhatják a vezérlőfolyamat-szerkezeteket for .. in .. do, while .. do, , try .. finally ..try .. with .., 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.

É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élda:

let makeTask() =
    task { return 1 }

makeTask() |> ValueTask<int>

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élda:

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ó.

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.

Feljegyzé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élda:

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.

Lásd még