Výrazy úkolů

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!. Výsledkem let! je umožnit provádění pokračovat v jiných výpočtech nebo vláknech při provádění výpočtů. Po pravé straně let! vazby se zbytek úlohy obnoví v provádě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! spuštění úkolu 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í:

return Výrazy

Ve výrazech return expr úkolů se používá k vrácení výsledku úkolu.

return! Výrazy

Ve výrazech return! expr úkolu se používá k vrácení výsledku jiného úkolu. Je ekvivalentní použití let! a okamžitě vrátí výsledek.

Tok řízení

Výrazy úloh mohou zahrnovat konstrukty for .. in .. dotoku řízení , while .. do, try .. with .., try .. finally .., , if .. then .. else, a if .. then ... Ty pak mohou zahrnovat další konstruktory úloh s výjimkou withfinally obslužných rutin, 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

Vazby v rámci výrazů use úkolů mohou svázat s hodnotami typu IDisposable nebo IAsyncDisposable. V případě druhé operace čištění odstranění se provádí asynchronně.

Kromě let!toho můžete provádět use! asynchronní vazby. Rozdíl mezi let! a use! je stejný jako rozdíl mezi let a use. Pro use!, objekt je uvolněn na konci aktuálního oboru. Všimněte si, use! že v jazyce F# 6 neumožňuje inicializaci hodnoty na hodnotu null, i když use ano.

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 úkol je dočasný hodnota, která je převedena na skutečný úkol pomocí .AsTask().

Chcete-li vytvořit úkol hodnoty z výrazu úkolu, použijte |> ValueTask<ReturnType> nebo |> ValueTask. Příklad:

let makeTask() =
    task { return 1 }

makeTask() |> ValueTask<int>

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í. Pří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í, SynchronizationContext.Current pokud jsou k dispozici. To umožňuje, aby úlohy sloužily jako spolupracující, prokládání agentů spouštěné na vlákně uživatelského rozhraní bez blokování uživatelského rozhraní. Pokud není k dispozici, pokračování úkolů jsou naplánována do 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 všechny SynchronizationContext.Current v následujícím smyslu: pokud je spuštěno ve vlákně s jinou hodnotou než null 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í, která ConfigureAwait(false) nejsou obvykle potřeba v kódu úlohy jazyka F#. Místo toho by měly být úkoly, které mají být spuštěny na pozadí, vytvořené pomocí backgroundTask { ... }. Všechny vnější vazby úkolu na pozadí se znovu synchronizují s SynchronizationContext.Current dokončením ú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í tailcalls. To znamená, že při return! spuštění je aktuální úkol registrován jako čeká 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ány asynchronní chvosty, použijte asynchronní výraz jazyka F#, který podporuje tailcalls. Pří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 dokumentu RFC s možností obnovení a v komunitní relaci kompilátoru jazyka F#.

Viz také