Aszinkron programozás az F-ben#
Az aszinkron programozás olyan mechanizmus, amely különböző okokból elengedhetetlen a modern alkalmazásokhoz. A fejlesztők többsége két elsődleges használati esetet fog tapasztalni:
- Olyan kiszolgálói folyamat bemutatása, amely jelentős számú egyidejű bejövő kérést képes kiszolgálni, miközben a kérelemfeldolgozás során foglalt rendszererőforrások minimalizálása az adott folyamaton kívüli rendszerek vagy szolgáltatások bemeneteit várja
- Rugalmas felhasználói felület vagy főszál karbantartása, miközben párhuzamosan halad a háttérmunka
Bár a háttérmunka gyakran több szál használatát is magában foglalja, fontos, hogy külön vegye figyelembe az aszinkron és a többszálas kapcsolat fogalmait. Valójában külön aggodalmak, és az egyik nem utal a másikra. Ez a cikk részletesebben ismerteti a különálló fogalmakat.
Aszinkron definíció
Az előző pont - hogy az aszinkronság független a több szál felhasználásától - érdemes egy kicsit tovább magyarázni. Három fogalom van, amelyek néha kapcsolódnak, de szigorúan függetlenek egymástól:
- Konkurencia; ha több számítás végrehajtása egymást átfedő időszakokban történik.
- Párhuzamosság; ha több számítás vagy egyetlen számítás több része pontosan ugyanabban az időben fut.
- Aszinkronizálás; ha egy vagy több számítás a fő programfolyamattól elkülönítve hajtható végre.
Mindhárom ortogonális fogalom, de könnyen elkonferálható, különösen akkor, ha együtt használják őket. Előfordulhat például, hogy több aszinkron számítást kell párhuzamosan végrehajtania. Ez a kapcsolat nem jelenti azt, hogy a párhuzamosság vagy az aszinkronság egymásra utal.
Ha figyelembe veszi az "aszinkron" szó etimológiáját, két részből áll:
- "a", azaz "nem".
- "szinkron", vagyis "egyidejűleg".
Ha összeadja ezt a két kifejezést, látni fogja, hogy az "aszinkron" azt jelenti, hogy "nem egyszerre". Ennyi az egész! Ebben a definícióban nincs hatással az egyidejűség vagy a párhuzamosság. Ez a gyakorlatban is igaz.
Gyakorlati szempontból az F# aszinkron számításait a program a fő programfolyamattól függetlenül hajtja végre. Ez a független végrehajtás nem jelent egyidejűséget vagy párhuzamosságot, és azt sem jelenti, hogy a számítás mindig a háttérben történik. Valójában az aszinkron számítások szinkron módon is végrehajthatók a számítás jellegétől és a számítás környezetétől függően.
A fő teendő az, hogy az aszinkron számítások függetlenek legyenek a fő programfolyamattól. Bár az aszinkron számítások végrehajtásának időpontjára és módjára kevés garancia van, van néhány módszer a vezénylésre és az ütemezésre. A cikk további része az F# aszinkronizálás alapfogalmait és az F#-ba beépített típusokat, függvényeket és kifejezéseket ismerteti.
Alapfogalmak
Az F#-ban az aszinkron programozás két alapvető fogalom köré épül: az aszinkron számításokra és feladatokra.
- A
Async<'T>
kifejezéseket tartalmazóasync { }
típus, amely egy feladat létrehozásához elindítható, komposztábilis aszinkron számítást jelöl. - A
Task<'T>
.NET-feladatot végrehajtó kifejezést tartalmazó típustask { }
.
Általában érdemes megfontolni az új kódban való használatot task {…}
async {…}
, ha feladatokat használó .NET-kódtárakkal dolgozik, és nem támaszkodik az aszinkron kódszkreditálásokra vagy implicit lemondási jogkivonat propagálására.
Az aszinkron alapfogalmai
Az "async" programozás alapfogalmait az alábbi példában tekintheti meg:
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
A példában a printTotalFileBytesUsingAsync
függvény típusa string -> Async<unit>
. A függvény meghívása valójában nem hajtja végre az aszinkron számítást. Ehelyett egy Async<unit>
olyan értéket ad vissza, amely az aszinkron végrehajtáshoz szükséges munka specifikációjaként működik. Async.AwaitTask
Meghívja a törzsét, amely az eredményt ReadAllBytesAsync megfelelő típussá alakítja.
Egy másik fontos sor a hívás.Async.RunSynchronously
Ez az aszinkron modul egyik kezdő függvénye, amelyet meg kell hívnia, ha ténylegesen F# aszinkron számítást szeretne végrehajtani.
Ez alapvető különbség a C#/Visual Basic programstílusban async
. Az F#-ban az aszinkron számítások hideg tevékenységeknek tekinthetők. A végrehajtást explicit módon kell elkezdeni. Ennek van néhány előnye, mivel lehetővé teszi az aszinkron munka összevonását és sorrendjét sokkal egyszerűbben, mint a C# vagy a Visual Basic esetén.
Aszinkron számítások kombinálása
Íme egy példa, amely az előzőre épül számítások kombinálásával:
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
Mint látható, a main
függvénynek több eleme is van. Elméletileg a következőket teszi:
- A parancssori argumentumok átalakítása számítások sorozatává
Async<unit>
a következővelSeq.map
: . - Hozzon létre egy olyant
Async<'T[]>
, amely a futtatáskor párhuzamosan ütemezi és futtatja aprintTotalFileBytes
számításokat. - Hozzon létre egy olyant
Async<unit>
, amely futtatja a párhuzamos számítást, és figyelmen kívül hagyja annak eredményét (ami egyunit[]
). - Explicit módon futtassa a teljes komponált számítást a , blokkolással
Async.RunSynchronously
, amíg be nem fejeződik.
A program futtatásakor printTotalFileBytes
az egyes parancssori argumentumok párhuzamosan futnak. Mivel az aszinkron számítások a programfolyamattól függetlenül futnak, nincs meghatározott sorrend, amelyben kinyomtatják az adataikat, és befejezik a végrehajtást. A számítások párhuzamosan lesznek ütemezve, de a végrehajtás sorrendje nem garantált.
Szekvencia-aszinkron számítások
Mivel Async<'T>
a munka specifikációja nem egy már futó feladat, egyszerűbben hajthat végre bonyolult átalakításokat. Íme egy példa, amely aszinkron számítások egy készletét sorrendbe állítja, így egymás után hajtják végre őket.
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
Ez az ütemezés printTotalFileBytes
szerint az elemek argv
sorrendjében lesz végrehajtva ahelyett, hogy párhuzamosan ütemezze őket. Mivel az egymást követő műveletek csak az előző számítás végrehajtása után lesznek ütemezve, a számítások úgy vannak rendezve, hogy a végrehajtásuk ne legyen átfedésben.
Fontos Async-modulfüggvények
Amikor Aszinkron kódot ír az F#-ban, általában olyan keretrendszerrel fog működni, amely kezeli a számítások ütemezését. Ez azonban nem mindig így van, ezért érdemes megismerni az aszinkron munka ütemezéséhez használható különböző függvényeket.
Mivel az F# aszinkron számítások a munka specifikációi , nem pedig a már végrehajtó munka ábrázolása, ezért explicit módon kell kezdeni őket egy kezdő függvénnyel. Számos aszinkron indítási módszer létezik, amelyek különböző kontextusokban hasznosak. Az alábbi szakasz a leggyakoribb kezdő függvények némelyikét ismerteti.
Async.StartChild
Elindít egy gyermekszámítást egy aszinkron számításon belül. Ez lehetővé teszi több aszinkron számítás egyidejű végrehajtását. A gyermekszámítás egy lemondási jogkivonatot oszt meg a szülőszámítással. Ha a szülőszámítást megszakítja, a gyermekszámítás is megszűnik.
Aláírás:
computation: Async<'T> * ?millisecondsTimeout: int -> Async<Async<'T>>
A következő esetekben használja:
- Ha egyszerre több aszinkron számítást szeretne egyszerre végrehajtani, de nem szeretné párhuzamosan ütemezni őket.
- Ha a gyermekszámítás élettartamát a szülőszámításhoz szeretné kötni.
Mire figyeljen a következőre:
- Több számítás
Async.StartChild
indítása nem ugyanaz, mint a párhuzamos ütemezés. Ha párhuzamosan szeretné ütemezni a számításokat, használjaAsync.Parallel
a következőt: . - A szülőszámítás lemondása az összes megkezdett gyermekszámítás lemondását váltja ki.
Async.StartImmediate
Aszinkron számítást futtat, amely azonnal elindul az aktuális operációs rendszer szálán. Ez akkor hasznos, ha frissítenie kell valamit a hívó szálon a számítás során. Ha például egy aszinkron számításnak frissítenie kell egy felhasználói felületet (például frissítenie kell egy folyamatjelző sávot), akkor Async.StartImmediate
azt kell használni.
Aláírás:
computation: Async<unit> * ?cancellationToken: CancellationToken -> unit
A következő esetekben használja:
- Ha frissítenie kell valamit a hívó szálon egy aszinkron számítás közepén.
Mire figyeljen a következőre:
- Az aszinkron számításban lévő kód minden olyan szálon fut, amelyen az egyik éppen be van ütemezve. Ez problémás lehet, ha a szál valamilyen módon érzékeny, például egy felhasználói felületi szál. Ilyen esetekben
Async.StartImmediate
valószínűleg nem megfelelő a használata.
Async.StartAsTask
Végrehajt egy számítást a szálkészletben. Task<TResult> A megfelelő állapotban befejezett eredményt ad vissza, miután a számítás leáll (létrehozza az eredményt, kivételt ad ki vagy megszakítja). Ha nincs megadva lemondási jogkivonat, akkor a rendszer az alapértelmezett lemondási jogkivonatot használja.
Aláírás:
computation: Async<'T> * ?taskCreationOptions: TaskCreationOptions * ?cancellationToken: CancellationToken -> Task<'T>
A következő esetekben használja:
- Ha be kell hívnia egy .NET API-t, amely aszinkron Task<TResult> számítások eredményét jeleníti meg.
Mire figyeljen a következőre:
- Ez a hívás egy további
Task
objektumot foglal le, amely növelheti a többletterhelést, ha gyakran használják.
Async.Parallel
Az aszinkron számítások sorozatát ütemezi, amelyeket párhuzamosan kell végrehajtani, és az eredmények tömbjét adja meg a megadott sorrendben. A párhuzamosság mértéke igény szerint hangolható/szabályozható a maxDegreeOfParallelism
paraméter megadásával.
Aláírás:
computations: seq<Async<'T>> * ?maxDegreeOfParallelism: int -> Async<'T[]>
Mikor érdemes használni:
- Ha egyszerre kell futtatnia egy számításkészletet, és nem kell függenie a végrehajtás sorrendjéről.
- Ha nem követeli meg a párhuzamosan ütemezett számítások eredményeit, amíg az összes befejeződött.
Mire figyeljen a következőre:
- Az eredményként kapott értéktömbhöz csak akkor férhet hozzá, ha az összes számítás befejeződött.
- A számítások minden alkalommal le lesznek futtatva, amikor végül ütemezettek lesznek. Ez a viselkedés azt jelenti, hogy nem támaszkodhat a végrehajtásuk sorrendjére.
Async.Sequential
Aszinkron számítások sorozatát ütemezi, amelyeket az átadásuk sorrendjében kell végrehajtani. Az első számítást végrehajtjuk, majd a következőt, és így tovább. A számítások párhuzamosan nem lesznek végrehajtva.
Aláírás:
computations: seq<Async<'T>> -> Async<'T[]>
Mikor érdemes használni:
- Ha több számítást kell végrehajtania sorrendben.
Mire figyeljen a következőre:
- Az eredményként kapott értéktömbhöz csak akkor férhet hozzá, ha az összes számítás befejeződött.
- A számítások a függvénynek átadott sorrendben lesznek futtatva, ami azt jelentheti, hogy az eredmények visszaadása előtt több idő telik el.
Async.AwaitTask
Egy aszinkron számítást ad vissza, amely megvárja, amíg a megadott Task<TResult> befejeződik, és eredményként adja vissza Async<'T>
Aláírás:
task: Task<'T> -> Async<'T>
A következő esetekben használja:
- Ha olyan .NET API-t használ, amely egy F# aszinkron számításon belül ad vissza egy Task<TResult> értéket.
Mire figyeljen a következőre:
- A kivételek a feladat párhuzamos kódtárának konvencióját követve vannak becsomagolva AggregateException . Ez a viselkedés eltér attól, hogy az F# async általában hogyan fedi fel a kivételeket.
Async.Catch
Létrehoz egy aszinkron számítást, amely végrehajt egy adott Async<'T>
, visszaadott Async<Choice<'T, exn>>
. Ha a megadott Async<'T>
művelet sikeresen befejeződött, a függvény az eredményül kapott értékkel ad vissza egy Choice1Of2
értéket. Ha a kivétel a befejezés előtt ki van dobva, akkor a rendszer a létrehozott kivétellel ad vissza egy Choice2of2
kivételt. Ha egy olyan aszinkron számításhoz használják, amely önmagában sok számításból áll, és az egyik számítás kivételt jelent, a teljes számítás leáll.
Aláírás:
computation: Async<'T> -> Async<Choice<'T, exn>>
A következő esetekben használja:
- Ha olyan aszinkron munkát végez, amely kivétellel meghiúsulhat, és ezt a kivételt a hívóban szeretné kezelni.
Mire figyeljen a következőre:
- Kombinált vagy szekvenált aszinkron számítások használatakor az átfogó számítás teljesen leáll, ha az egyik "belső" számítás kivételt jelent.
Async.Ignore
Létrehoz egy aszinkron számítást, amely az adott számítást futtatja, de elveti az eredményt.
Aláírás:
computation: Async<'T> -> Async<unit>
A következő esetekben használja:
- Ha olyan aszinkron számítással rendelkezik, amelynek az eredménye nem szükséges. Ez hasonló a
ignore
nem aszinkron kód függvényéhez.
Mire figyeljen a következőre:
- Ha azért kell használnia
Async.Ignore
, mert használni szeretnéAsync.Start
, vagy egy másik függvényt igényelAsync<unit>
, fontolja meg, hogy az eredmény elvetése rendben van-e. Kerülje az eredmények elvetését, csak a típusadákulatok elférése érdekében.
Async.RunSynchronously
Aszinkron számítást futtat, és várja az eredményét a hívó szálon. Kivétel propagálása, ha a számítás egy eredményt ad. Ez a hívás blokkolva van.
Aláírás:
computation: Async<'T> * ?timeout: int * ?cancellationToken: CancellationToken -> 'T
Mikor érdemes használni:
- Ha szüksége van rá, csak egyszer használja egy alkalmazásban – a végrehajtható fájl belépési pontján.
- Ha nem érdekli a teljesítmény, és egy sor más aszinkron műveletet szeretne egyszerre végrehajtani.
Mire figyeljen a következőre:
- A hívás
Async.RunSynchronously
letiltja a hívó szálat, amíg a végrehajtás befejeződik.
Async.Start
Elindít egy aszinkron számítást, amely a szálkészletben tér vissza unit
. Nem várja meg a befejezést, és/vagy nem figyeli meg a kivétel kimenetelét. A beágyazott számításokat Async.Start
a rendszer az őket elnevezett szülőszámítástól függetlenül indítja el, élettartamuk nincs szülőszámításhoz kötve. Ha a szülőszámítás megszakad, a gyermekszámítások nem lesznek megszakítva.
Aláírás:
computation: Async<unit> * ?cancellationToken: CancellationToken -> unit
Csak akkor használja, ha:
- Olyan aszinkron számítással rendelkezik, amely nem eredményez eredményt, és/vagy feldolgozást igényel.
- Nem kell tudnia, hogy mikor fejeződik be az aszinkron számítás.
- Nem érdekli, hogy melyik szálon fut egy aszinkron számítás.
- Nem kell tisztában lennie a végrehajtásból eredő kivételekkel vagy jelentésekkel.
Mire figyeljen a következőre:
- A kezdő számítások által kiváltott kivételeket
Async.Start
a hívó nem propagálja. A hívásverem teljesen fel lesz oldva. - Minden olyan munka (például hívás
printfn
), amely aAsync.Start
program végrehajtásának fő szálára hatással van, nem fog bekövetkezni.
Együttműködés a .NET-tel
Programozás használata esetén async { }
előfordulhat, hogy aszinkron/várakozás stílusú aszinkron programozást használó .NET-kódtárral vagy C#-kódbázissal kell együttműködnie. Mivel a C# és a .NET-kódtárak többsége alapvető absztrakcióként használja a Task<TResult> típusokat, Task ez megváltoztathatja az F# aszinkron kód írását.
Az egyik lehetőség a .NET-feladatok írása közvetlenül task { }
a . Másik lehetőségként használhatja a Async.AwaitTask
függvényt egy .NET aszinkron számításra:
let getValueFromLibrary param =
async {
let! value = DotNetLibrary.GetValueAsync param |> Async.AwaitTask
return value
}
A Async.StartAsTask
függvény használatával aszinkron számítást adhat át egy .NET-hívónak:
let computationForCaller param =
async {
let! result = getAsyncResult param
return result
} |> Async.StartAsTask
Ha olyan API-kat szeretne használni Task (azaz .NET aszinkron számításokat, amelyek nem adnak vissza értéket), előfordulhat, hogy hozzá kell adnia egy további függvényt, amely átalakítja TaskAsync<'T>
a következőt:
module Async =
// Async<unit> -> Task
let startTaskFromAsyncUnit (comp: Async<unit>) =
Async.StartAsTask comp :> Task
Már van olyan, Async.AwaitTask
amely bemenetként fogad el egy Task értéket. Ezzel és a korábban definiált startTaskFromAsyncUnit
függvénnyel megkezdheti és várhatja Task a típusokat egy F# aszinkron számításból.
.NET-feladatok írása közvetlenül F nyelven#
Az F#-ban közvetlenül task { }
is írhat feladatokat, például:
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
A példában a printTotalFileBytesUsingTasks
függvény típusa string -> Task<unit>
. A függvény meghívása elkezdi végrehajtani a feladatot.
A hívás megvárja task.Wait()
a feladat befejezését.
Többszálas kapcsolat
Bár ebben a cikkben a szálkezelésről van szó, két fontos dologra kell emlékezni:
- Nincs affinitás az aszinkron számítás és a szál között, kivéve, ha az aktuális szálon kifejezetten elindult.
- Az Aszinkron programozás az F#-ban nem absztrakció a többszálas használathoz.
Előfordulhat például, hogy a számítás a hívó szálán fut, a munka jellegétől függően. A számítások is "ugrik" között szálak, kölcsön őket egy kis ideig, hogy hasznos munkát között "várakozás" (például amikor egy hálózati hívás van folyamatban).
Bár az F# lehetővé teszi az aszinkron számítások indítását az aktuális szálon (vagy kifejezetten nem az aktuális szálon), az aszinkronság általában nincs társítva egy adott szálkészítési stratégiával.
Lásd még
Visszajelzés
https://aka.ms/ContentUserFeedback.
Hamarosan elérhető: 2024-ben fokozatosan kivezetjük a GitHub-problémákat a tartalom visszajelzési mechanizmusaként, és lecseréljük egy új visszajelzési rendszerre. További információ:Visszajelzés küldése és megtekintése a következőhöz: