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.Result
a . 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:
- .NET-feladatok és Task<TResult> a nem általános Task.
- .NET-értékfeladatok és ValueTask<TResult> nem általános ValueTask.
- F# aszinkron számítások
Async<T>
. - Az F# RFC FS-1097-ben megadott "GetAwaiter" mintát követő objektumok.
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ú IAsyncDisposable
objektummal 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 use
a . 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.Current
szá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.