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.
Asynchronní programování je mechanismus, který je nezbytný pro moderní aplikace z různých důvodů. Většina vývojářů se setká se dvěma hlavními případy použití:
- Představujeme proces serveru, který může obsluhovat značný počet souběžných příchozích požadavků, při současném minimalizování systémových prostředků využívaných během čekání na vstupy ze systémů nebo služeb, které jsou externí vůči tomuto procesu.
- Udržování responzivního uživatelského rozhraní nebo hlavního vlákna při souběžné práci na pozadí
I když práce na pozadí často zahrnuje využití více vláken, je důležité vzít v úvahu koncepty asynchrony a více vláken samostatně. Ve skutečnosti se jedná o samostatné obavy a jeden z nich neznamená druhý. Tento článek podrobněji popisuje samostatné koncepty.
Asynchronicita definována
Předchozí bod - že asynchronnost je nezávislá na využití více vláken - stojí za to podrobněji vysvětlit. Existují tři koncepty, které někdy souvisejí, ale přísně nezávislé na sobě:
- Souběžnost; při provádění více výpočtů v překrývajících se časových obdobích.
- Rovnoběžnost; při spuštění více výpočtů nebo několika částí jednoho výpočtu ve stejnou dobu.
- Asynchronní; pokud se jeden nebo více výpočtů může spouštět odděleně od hlavního toku programu.
Všechny tři jsou ortogonální koncepty, ale mohou být snadno zaměněny, zejména když se používají společně. Možná budete například muset paralelně spustit několik asynchronních výpočtů. Tento vztah neznamená, že paralelismus nebo asynchronie vzájemně implikují jeden druhého.
Pokud uvažujete o etymologii slova "asynchronní", existují dvě části:
- "a", což znamená "ne".
- "synchronní", což znamená "ve stejnou dobu".
Když tyto dva termíny spojíte dohromady, uvidíte, že "asynchronní" znamená "ne ve stejnou dobu". To je to! V této definici neexistuje žádný implikace souběžnosti ani paralelismu. To platí i v praxi.
V praxi jsou asynchronní výpočty v jazyce F# naplánovány tak, aby se spouštěly nezávisle na hlavním toku programu. Toto nezávislé spuštění neznamená souběžnost ani paralelismus, ani neznamená, že výpočet se vždy děje na pozadí. Asynchronní výpočty se můžou dokonce spouštět synchronně v závislosti na povaze výpočtu a prostředí, ve které se výpočetní výkon provádí.
Hlavní poznatky, které byste měli mít, je, že asynchronní výpočty jsou nezávislé na hlavním toku programu. I když existuje několik záruk o tom, kdy nebo jak se provádí asynchronní výpočty, existují některé přístupy k jejich orchestraci a plánování. Zbytek tohoto článku popisuje základní koncepty asynchronní synchronizace jazyka F# a způsob použití typů, funkcí a výrazů integrovaných do jazyka F#.
Klíčové koncepty
V jazyce F# je asynchronní programování zaměřené na dva základní koncepty: asynchronní výpočty a úlohy.
- Typ
Async<'T>sasync { }výrazy, který představuje kompozibilní asynchronní výpočty, které lze spustit pro vytvoření úkolu. - Typ
Task<'T>, který zahrnujetask { }výrazy, představující spuštěnou úlohu .NET.
Obecně byste měli zvážit použití task {…} namísto async {…} v novém kódu, pokud spolupracujete s knihovnami .NET, které používají vlákna, a pokud nespoléháte na asynchronní koncová volání nebo implicitní šíření tokenů zrušení.
Základní koncepty async
Základní koncepty "asynchronního" programování si můžete prohlédnout v následujícím příkladu:
open System
open System.IO
// Perform an asynchronous read of a file using 'async'
let printTotalFileBytesUsingAsync (path: string) =
async {
let! bytes = File.ReadAllBytesAsync(path) |> Async.AwaitTask
let fileName = Path.GetFileName(path)
printfn $"File {fileName} has %d{bytes.Length} bytes"
}
[<EntryPoint>]
let main argv =
printTotalFileBytesUsingAsync "path-to-file.txt"
|> Async.RunSynchronously
Console.Read() |> ignore
0
V příkladu printTotalFileBytesUsingAsync je funkce typu string -> Async<unit>. Volání funkce ve skutečnosti nespustí asynchronní výpočty. Místo toho vrátí Async<unit> funkci, která funguje jako specifikace práce, která se má spouštět asynchronně. Volá Async.AwaitTask ve svém těle, což převádí výsledek ReadAllBytesAsync na odpovídající typ.
Dalším důležitým řádkem je volání Async.RunSynchronously. Jedná se o jednu z funkcí, které spouští asynchronní modul, které budete muset volat, pokud chcete skutečně spustit asynchronní výpočet jazyka F#.
Jedná se o základní rozdíl ve stylu async programování jazyka C#/Visual Basic. V jazyce F# si asynchronní výpočty můžete představit jako studené úlohy. Je potřeba je výslovně spustit, aby mohly opravdu běžet. To má určité výhody, protože umožňuje kombinovat a sekvencovat asynchronní práci mnohem snadněji než v jazyce C# nebo Visual Basic.
Kombinování asynchronních výpočtů
Tady je příklad, který vychází z předchozího příkladu zkombinováním výpočtů:
open System
open System.IO
let printTotalFileBytes path =
async {
let! bytes = File.ReadAllBytesAsync(path) |> Async.AwaitTask
let fileName = Path.GetFileName(path)
printfn $"File {fileName} has %d{bytes.Length} bytes"
}
[<EntryPoint>]
let main argv =
argv
|> Seq.map printTotalFileBytes
|> Async.Parallel
|> Async.Ignore
|> Async.RunSynchronously
0
Jak vidíte, main funkce má několik dalších prvků. Koncepčně provede následující:
- Transformujte argumenty příkazového
Async<unit>řádku na sekvenci výpočtů pomocíSeq.map. - Vytvořte
Async<'T[]>, který plánuje a spouštíprintTotalFileBytesvýpočty paralelně během svého běhu. - Vytvořte
Async<unit>, která spustí paralelní výpočet a ignoruje jeho výsledek (což jeunit[]). - Explicitně spusťte celkový složený výpočet s
Async.RunSynchronouslya pokračujte blokováním, dokud se nedokončí.
Když se tento program spustí, printTotalFileBytes spustí se paralelně pro každý argument příkazového řádku. Vzhledem k tomu, že asynchronní výpočty se spouštějí nezávisle na toku programu, není definováno žádné pořadí, ve kterém vytisknou informace a dokončí provádění. Výpočty budou naplánovány paralelně, ale jejich pořadí provádění není zaručeno.
Sekvenční asynchronní výpočty
Vzhledem k tomu Async<'T> , že se jedná o specifikaci práce místo již spuštěné úlohy, můžete snadno provádět složitější transformace. Tady je příklad, který sekvencuje sadu asynchronních výpočtů, aby se jedna po druhé spustila.
let printTotalFileBytes path =
async {
let! bytes = File.ReadAllBytesAsync(path) |> Async.AwaitTask
let fileName = Path.GetFileName(path)
printfn $"File {fileName} has %d{bytes.Length} bytes"
}
[<EntryPoint>]
let main argv =
argv
|> Seq.map printTotalFileBytes
|> Async.Sequential
|> Async.Ignore
|> Async.RunSynchronously
|> ignore
Tím se naplánuje printTotalFileBytes ke spuštění v pořadí prvků argv místo jejich paralelního naplánování. Vzhledem k tomu, že každá následná operace nebude naplánovaná až po dokončení předchozího výpočtu, jsou výpočty sekvencovány tak, aby se jejich provádění nepřekrývaly.
Důležité funkce modulu Async
Při psaní asynchronního kódu v jazyce F# budete obvykle pracovat s architekturou, která zpracovává plánování výpočtů za vás. To však není vždy případ, takže je dobré pochopit různé funkce, které lze použít k naplánování asynchronní práce.
Vzhledem k tomu, že asynchronní výpočty jazyka F# představují specifikaci práce, nikoli reprezentaci již spuštěné práce, musí být explicitně spuštěny s počáteční funkcí. Existuje mnoho asynchronních metod spouštění , které jsou užitečné v různých kontextech. Následující část popisuje některé z nejběžnějších spouštěcích funkcí.
Async.StartChild
Spustí podřízený výpočet v rámci asynchronního výpočtu. To umožňuje souběžné spouštění několika asynchronních výpočtů. Podřízená výpočetní úloha sdílí token zrušení s nadřazenou výpočetní úlohou. Pokud je nadřazený výpočet zrušen, je také zrušen podřízený výpočet.
Podpis:
computation: Async<'T> * ?millisecondsTimeout: int -> Async<Async<'T>>
Kdy použít:
- Pokud chcete souběžně spouštět více asynchronních výpočtů, nikoli jeden současně, ale nechcete je mít naplánované paralelně.
- Pokud chcete svázat životnost podřízeného výpočtu s nadřazeným výpočtem.
Na co si dát pozor:
- Zahájení několika výpočtů s
Async.StartChildnení totéž jako jejich plánování v paralelním režimu. Pokud chcete plánovat výpočty paralelně, použijteAsync.Parallel. - Zrušením nadřazeného výpočtu dojde ke zrušení všech podřízených výpočtů, které spustil.
Async.StartImmediate
Spustí asynchronní výpočet, který se spustí okamžitě na aktuálním vlákně operačního systému. To je užitečné, pokud potřebujete během výpočtu něco aktualizovat ve volajícím vlákně. Pokud například asynchronní výpočet musí aktualizovat uživatelské rozhraní (například aktualizovat indikátor průběhu), Async.StartImmediate měl by se použít.
Podpis:
computation: Async<unit> * ?cancellationToken: CancellationToken -> unit
Kdy použít:
- Když potřebujete něco aktualizovat ve volajícím vlákně uprostřed asynchronního výpočtu.
Na co si dát pozor:
- Kód v asynchronním výpočtu poběží na jakémkoli vlákně, na které bude zrovna naplánován. To může být problematické, pokud je vlákno nějakým způsobem citlivé, například vlákno uživatelského rozhraní. V takových případech
Async.StartImmediateje pravděpodobně nevhodné použít.
Async.StartAsTask
Vykoná výpočet ve fondu vláken. Task<TResult> Vrátí hodnotu, která bude dokončena v odpovídajícím stavu po ukončení výpočtu (vytvoří výsledek, vyvolá výjimku nebo se zruší). Pokud není zadaný žádný token zrušení, použije se výchozí token zrušení.
Podpis:
computation: Async<'T> * ?taskCreationOptions: TaskCreationOptions * ?cancellationToken: CancellationToken -> Task<'T>
Kdy použít:
- Když potřebujete zavolat rozhraní .NET API, které poskytuje Task<TResult> k reprezentaci výsledku asynchronního výpočtu.
Na co si dát pozor:
- Toto volání přidělí další
Taskobjekt, který může zvýšit režii, pokud je často používán.
Async.Parallel
Naplánuje sekvenci asynchronních výpočtů, které se mají spouštět paralelně a poskytují pole výsledků v pořadí, v jakém byly zadány. Stupeň paralelismu lze volitelně ladit nebo omezovat zadáním parametru maxDegreeOfParallelism .
Podpis:
computations: seq<Async<'T>> * ?maxDegreeOfParallelism: int -> Async<'T[]>
Kdy ji použít:
- Pokud potřebujete spustit sadu výpočtů najednou a nemusíte se spoléhat na jejich pořadí provádění.
- Pokud nevyžadujete výsledky z výpočtů naplánovaných paralelně, dokud se nedokončí všechny.
Na co si dát pozor:
- Výslednou matici hodnot můžete získat pouze po dokončení všech výpočtů.
- Výpočty budou spuštěny, jakmile budou naplánovány. Toto chování znamená, že nemůžete spoléhat na jejich pořadí provádění.
Async.Sekvenční
Naplánuje sekvenci asynchronních výpočtů, které se mají spustit v pořadí, v jakém jsou předány. První výpočet se spustí, pak další atd. Nespustí se paralelně žádné výpočty.
Podpis:
computations: seq<Async<'T>> -> Async<'T[]>
Kdy ji použít:
- Pokud potřebujete provést více výpočtů v pořadí.
Na co si dát pozor:
- Výslednou matici hodnot můžete získat pouze po dokončení všech výpočtů.
- Výpočty budou spuštěny v pořadí, v jakém jsou předány této funkci, což může znamenat, že více času uplyne před vrácením výsledků.
Async.AwaitTask
Vrátí asynchronní výpočet, který čeká na dokončení dané hodnoty Task<TResult> a vrátí výsledek jako Async<'T>
Podpis:
task: Task<'T> -> Async<'T>
Kdy použít:
- Pokud používáte rozhraní .NET API, které vrací Task<TResult> v rámci asynchronního výpočtu jazyka F#.
Na co si dát pozor:
- Výjimky jsou zabalené podle AggregateException konvence paralelní knihovny úloh. Toto chování se liší od toho, jak asynchronní jazyk F# obecně zpřístupní výjimky.
Async.Catch
Vytvoří asynchronní výpočet, který spustí danou Async<'T> a vrátí Async<Choice<'T, exn>>. Pokud se daný Async<'T> úspěšně dokončí, je vrácena Choice1Of2 s výslednou hodnotou. Pokud je před dokončením vyvolána výjimka, vrátí se Choice2of2 s danou výjimkou. Pokud se používá u asynchronního výpočtu, který se skládá z mnoha výpočtů, a jeden z těchto výpočtů vyvolá výjimku, zahrnující výpočty se úplně zastaví.
Podpis:
computation: Async<'T> -> Async<Choice<'T, exn>>
Kdy použít:
- Při provádění asynchronní práce, která může selhat kvůli výjimce a chcete tuto výjimku zpracovat ve volajícím.
Na co si dát pozor:
- Při použití kombinovaných nebo sekvenčních asynchronních výpočtů se celkový výpočet úplně zastaví, pokud některý z jeho "interních" výpočtů vyvolá výjimku.
Async.Ignore
Vytvoří asynchronní výpočet, který spustí daný výpočet, ale sníží jeho výsledek.
Podpis:
computation: Async<'T> -> Async<unit>
Kdy použít:
- Pokud máte asynchronní výpočet, jehož výsledek není potřeba. To je podobné funkci
ignorepro nesynchronní kód.
Na co si dát pozor:
- Pokud je nutné použít
Async.Ignore, protože chcete použítAsync.Startnebo jinou funkci, která vyžadujeAsync<unit>, zvažte, zda je odstranění výsledku v pořádku. Vyhněte se vyřazování výsledků jen proto, aby odpovídaly signatuře typu.
Async.RunSynchronously
Spustí asynchronní výpočet a očekává jeho výsledek ve volajícím vlákně. Rozšíří výjimku, pokud výpočet vyvolá výjimku. Toto volání je blokující.
Podpis:
computation: Async<'T> * ?timeout: int * ?cancellationToken: CancellationToken -> 'T
Kdy ji použít:
- Pokud ho potřebujete, použijte ho jenom jednou v aplikaci – v vstupním bodě spustitelného souboru.
- Pokud vás nezajímá výkon a chcete spustit sadu dalších asynchronních operací najednou.
Na co si dát pozor:
- Volání
Async.RunSynchronouslyblokuje volající vlákno, dokud provádění neskončí.
Async.Start
Spustí asynchronní výpočet, který se vrátí ve fondu vláken jako unit. Nečeká na jeho dokončení a/nebo nezohledňuje výsledek výjimky. Vnořené výpočty, které začínají s Async.Start, jsou spouštěny nezávisle na nadřazeném výpočtu, který je volal. Jejich životnost není svázána s žádným nadřazeným výpočtem. Pokud je nadřazený výpočet zrušen, nebudou zrušeny žádné podřízené výpočty.
Podpis:
computation: Async<unit> * ?cancellationToken: CancellationToken -> unit
Použít pouze v případech:
- Máte asynchronní výpočet, který nevyvolá výsledek nebo vyžaduje zpracování jednoho.
- Nemusíte vědět, kdy se dokončí asynchronní výpočet.
- Nezajímá vás, na kterém vlákně běží asynchronní výpočty.
- Nemusíte být informováni o výjimkách ani je hlásit, které vzniknou při provádění.
Na co si dát pozor:
- Výjimky vyvolané výpočty zahájenými s
Async.Startnejsou předávány volajícímu. Zásobník volání bude zcela rozvinut. - Jakákoli práce (například volání
printfn) spuštěná sAsync.Startnevyvolá efekt v hlavním vlákně programu.
Spolupráce s .NET
Pokud používáte async { } programování, možná budete muset spolupracovat s knihovnou .NET nebo kódovým základem jazyka C#, který využívá asynchronní programování ve stylu async/await. Vzhledem k tomu, že C# a většina knihoven .NET používají typy Task<TResult> a Task jako své základní abstrakce, může to změnit způsob, jakým píšete asynchronní kód v jazyce F#.
Jednou z možností je přepnout na zápis úloh .NET přímo pomocí task { }. Alternativně můžete použít Async.AwaitTask funkci k vyčkání asynchronního výpočtu .NET:
let getValueFromLibrary param =
async {
let! value = DotNetLibrary.GetValueAsync param |> Async.AwaitTask
return value
}
Funkci Async.StartAsTask můžete použít k předání asynchronního výpočtu volajícímu .NET.
let computationForCaller param =
async {
let! result = getAsyncResult param
return result
} |> Async.StartAsTask
Pokud chcete pracovat s rozhraními API, která používají Task (tj. asynchronní výpočty .NET, které nevrací hodnotu), možná budete muset přidat další funkci, která převede hodnotu Async<'T> na Task:
module Async =
// Async<unit> -> Task
let startTaskFromAsyncUnit (comp: Async<unit>) =
Async.StartAsTask comp :> Task
Existuje již objekt Async.AwaitTask , který přijímá Task jako vstup. S touto a dříve definovanou startTaskFromAsyncUnit funkcí můžete zahájit a čekat na Task typy z asynchronního výpočtu v jazyce F#.
Psaní úloh .NET přímo v jazyce F#
V jazyce F# můžete psát úkoly přímo pomocí task { }, například:
open System
open System.IO
/// Perform an asynchronous read of a file using 'task'
let printTotalFileBytesUsingTasks (path: string) =
task {
let! bytes = File.ReadAllBytesAsync(path)
let fileName = Path.GetFileName(path)
printfn $"File {fileName} has %d{bytes.Length} bytes"
}
[<EntryPoint>]
let main argv =
let task = printTotalFileBytesUsingTasks "path-to-file.txt"
task.Wait()
Console.Read() |> ignore
0
V příkladu printTotalFileBytesUsingTasks je funkce typu string -> Task<unit>. Volání funkce spustí úlohu.
Volání na task.Wait() čeká na dokončení úkolu.
Vztah k vícevláknovosti
I když je v tomto článku zmiňováno vlákna, je třeba pamatovat na dvě důležité věci:
- Mezi asynchronním výpočtem a vláknem neexistuje spřažení, pokud není explicitně spuštěno v aktuálním vlákně.
- Asynchronní programování v jazyce F# není abstrakcí pro více vláken.
Výpočet může například běžet ve vlákně volajícího v závislosti na povaze práce. Výpočet by také mohl "přeskakovat" mezi vlákny, dočasně je využívat k provádění užitečné práce v obdobích čekání (například při přenosu po síti).
I když jazyk F# poskytuje určité možnosti pro zahájení asynchronního výpočtu v aktuálním vlákně (nebo explicitně ne v aktuálním vlákně), asynchrony obecně není přidružená ke konkrétní strategii dělení na vlákna.