Poznámka
Přístup k této stránce vyžaduje autorizaci. Můžete se zkusit přihlásit nebo změnit adresáře.
Přístup k této stránce vyžaduje autorizaci. Můžete zkusit změnit adresáře.
Tento článek popisuje podporu jazyka F# pro výrazy úloh, které se podobají asynchronním výrazům , ale umožňují vytvářet úlohy .NET přímo. Stejně jako asynchronní výrazy provádějí výrazy úloh kód asynchronně, to znamená bez blokování provádění jiné práce.
Asynchronní kód je obvykle vytvořený pomocí asynchronních výrazů. Použití výrazů úloh se upřednostňuje při rozsáhlé spolupráci s knihovnami .NET, které vytvářejí nebo využívají úlohy .NET. Výrazy úloh můžou také zlepšit výkon a možnosti ladění. Výrazy úloh ale mají určitá omezení, která jsou popsána dále v článku.
Syntaxe
task { expression }
V předchozí syntaxi je výpočet reprezentovaný expression
nastaven tak, aby běžel jako úloha .NET. Úloha se spustí okamžitě po spuštění tohoto kódu a spustí se v aktuálním vlákně, dokud se neprovede první asynchronní operace (například asynchronní režim spánku, asynchronní vstupně-výstupní operace nebo jiná primitivní asynchronní operace). Typ výrazu je Task<'T>
, kde 'T
je typ vrácený výrazem při použití klíčového return
slova.
Vazby pomocí let!
Ve výrazu úlohy jsou některé výrazy a operace synchronní a některé jsou asynchronní. Když očekáváte výsledek asynchronní operace namísto obyčejné let
vazby, použijete let!
. Vliv let!
umožňuje, aby provádění pokračovalo na jiných výpočtech nebo vláknech, zatímco je provádění výpočtu realizováno. Jakmile se pravá strana let!
vazby vrátí, zbytek úlohy pokračuje ve vykonávání.
Následující kód ukazuje rozdíl mezi let
a let!
. Řádek kódu, který používá let
pouze vytvoří úlohu jako objekt, který můžete očekávat později pomocí, například task.Wait()
nebo task.Result
. Řádek kódu, který používá let!
, spouští úkol a očekává jeho výsledek.
// 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)
Výrazy jazyka F# task { }
mohou očekávat následující typy asynchronních operací:
- Úlohy .NET Task<TResult> a negenerické Task.
- Úkoly hodnot .NET, ValueTask<TResult> a negenerické ValueTask.
- Asynchronní výpočty
Async<T>
jazyka F# - Libovolný objekt za vzorem GetAwaiter zadaný v F# RFC FS-1097.
return
výrazy
Ve výrazech úkolů se return expr
používá k vrácení výsledku úkolu.
return!
výrazy
Ve výrazech úkolu se return! expr
používá k vrácení výsledku jiného úkolu. Je ekvivalentní použití let!
a okamžitě vrátí výsledek.
Řízení toku
Výrazy úloh mohou zahrnovat konstrukty for .. in .. do
toku řízení , while .. do
, try .. with ..
, try .. finally ..
, , if .. then .. else
, a if .. then ..
. Ty mohou zahrnovat další konstrukty úloh, s výjimkou obslužných rutin with
a finally
, které se provádějí synchronně. Pokud potřebujete asynchronní try .. finally ..
, použijte use
vazbu v kombinaci s objektem typu IAsyncDisposable
.
use
a use!
vazby
V rámci výrazů úkolů use
mohou vazby svázat s hodnotami typu IDisposable nebo IAsyncDisposable. U druhé možnosti se operace odstranění provádí asynchronně.
Kromě let!
toho můžete provádět asynchronní vazby pomocí use!
. Rozdíl mezi let!
a use!
je stejný jako rozdíl mezi let
a use
. Pro use!
, objekt je s uzavřením aktuálního oboru uvolněn. Všimněte si, use!
že v jazyce F# 6 neumožňuje inicializaci hodnoty na hodnotu null, i když use
ano.
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
}
Hodnotové úkoly
Hodnotové úkoly jsou struktury, které se používají k zabránění přidělení v programování založeném na úkolech. Hodnota úkolu je dočasnou hodnotou, která je převedena na skutečný úkol pomocí .AsTask()
.
Chcete-li vytvořit hodnotový úkol z výrazu úkolu, použijte |> ValueTask<ReturnType>
nebo |> ValueTask
. Například:
let makeTask() =
task { return 1 }
makeTask() |> ValueTask<int>
and!
vazby (počínaje jazykem F# 10)
Ve výrazech úloh je možné souběžně očekávat více asynchronních operací (Task<'T>
atdValueTask<'T>
Async<'T>
.). Porovnat:
// 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
}
Přidání tokenů zrušení a kontrol zrušení
Na rozdíl od asynchronních výrazů jazyka F# výrazy úloh implicitně nepřevádějí token zrušení a neprovádějí implicitně kontroly zrušení. Pokud váš kód vyžaduje token zrušení, měli byste jako parametr zadat token zrušení. Například:
open System.Threading
let someTaskCode (cancellationToken: CancellationToken) =
task {
cancellationToken.ThrowIfCancellationRequested()
printfn $"continuing..."
}
Pokud máte v úmyslu správně zrušit kód, pečlivě zkontrolujte, že token zrušení předáte všem operacím knihovny .NET, které podporují zrušení. Například Stream.ReadAsync
má více přetížení, z nichž jeden přijímá token zrušení. Pokud toto přetížení nepoužíváte, nebude možné tuto konkrétní asynchronní operaci čtení zrušit.
Úlohy na pozadí
Ve výchozím nastavení se úlohy .NET plánují pomocí SynchronizationContext.Current, pokud je to možné. To umožňuje, aby úlohy sloužily jako kooperující, prokládané úlohy spouštěné na vlákně uživatelského rozhraní bez jeho blokování. Pokud není přítomno, pokračování úloh je naplánováno ve fondu vláken .NET.
V praxi je často žádoucí, aby kód knihovny, který generuje úlohy, ignoroval kontext synchronizace a místo toho vždy přepne do fondu vláken .NET v případě potřeby. Můžete toho dosáhnout pomocí backgroundTask { }
:
backgroundTask { expression }
Úloha na pozadí ignoruje jakýkoliv SynchronizationContext.Current
v následujícím smyslu: pokud je spuštěna ve vlákně s nenulovou hodnotou SynchronizationContext.Current
, přepne na vlákno na pozadí ve fondu vláken pomocí Task.Run
. Pokud je spuštěno ve vlákně s hodnotou null SynchronizationContext.Current
, spustí se ve stejném vlákně.
Poznámka:
V praxi to znamená, že volání na ConfigureAwait(false)
nejsou v kódu úloh jazyka F# obvykle potřeba. Místo toho by měly být úkoly, které mají být spuštěny na pozadí, vytvořené pomocí backgroundTask { ... }
. Jakékoli vnější vazby k úkolu na pozadí se znovu synchronizují s SynchronizationContext.Current
po dokončení úlohy na pozadí.
Omezení úkolů týkajících se tailcalls
Na rozdíl od asynchronních výrazů jazyka F# výrazy úloh nepodporují rekurzivní volání. To znamená, že při return!
spuštění je aktuální úkol registrován jako čekající na úkol, jehož výsledek je vrácen. To znamená, že rekurzivní funkce a metody implementované pomocí výrazů úloh mohou vytvářet nevázané řetězy úkolů a mohou používat nevázaný zásobník nebo haldu. Představte si například následující kód:
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()
Tento styl kódování by neměl být použit s výrazy úkolů – vytvoří řetězec 10000000 úkolů a způsobí StackOverflowException
. Pokud se při každém vyvolání smyčky přidá asynchronní operace, kód použije v podstatě nevázanou haldu. Zvažte přepnutí tohoto kódu tak, aby používal explicitní smyčku, například:
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()
Pokud jsou vyžadována asynchronní volání ocasu, použijte asynchronní výraz v jazyce F#, který podporuje volání ocasu. Například:
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()
Implementace úkolů
Úlohy se implementují pomocí obnovitelného kódu, nové funkce v jazyce F# 6. Úlohy se kompilují do "Resumable State Machines" kompilátorem jazyka F#. Podrobně jsou popsány v RFC pro obnovitelný kód a na setkání komunity kolem kompilátoru jazyka F#.