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