Megjegyzés
Az oldalhoz való hozzáféréshez engedély szükséges. Megpróbálhat bejelentkezni vagy módosítani a címtárat.
Az oldalhoz való hozzáféréshez engedély szükséges. Megpróbálhatja módosítani a címtárat.
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:
- .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.
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 .