Megosztás a következőn keresztül:


Aszinkron programozás async és await kulcsszavakkal

A Tevékenység aszinkron programozás (TAP) modell absztrakciós réteget biztosít a tipikus aszinkron kódoláshoz. Ebben a modellben a kódot utasítások sorozataként kell írnia, ugyanúgy, mint a szokásos. A különbség az, hogy elolvashatja a feladatalapú kódot, amikor a fordító feldolgozza az egyes utasításokat, és mielőtt elkezdené feldolgozni a következő utasítást. A modell végrehajtásához a fordító számos átalakítást hajt végre az egyes feladatok elvégzéséhez. Egyes utasítások elindíthatják a munkát, és visszaadhatnak egy Task objektumot, amely a folyamatban lévő munkát jelöli, és a fordítónak meg kell oldania ezeket az átalakításokat. A feladat aszinkron programozásának célja, hogy olyan kódot engedélyezzen, amely utasítássorozatként olvas, de bonyolultabb sorrendben hajtja végre. A végrehajtás a külső erőforrás-kiosztáson és a tevékenységek befejezésén alapul.

A tevékenység aszinkron programozási modellje hasonló ahhoz, ahogyan az emberek utasításokat adnak az aszinkron feladatokat tartalmazó folyamatokhoz. Ez a cikk egy példát használ a reggeli elkészítésére, hogy bemutassa, hogyan könnyíti meg a async és await kulcsszavak használata az aszinkron utasításokat tartalmazó kódokkal kapcsolatos gondolkodást. A reggeli elkészítésére vonatkozó utasításokat listaként is meg lehet adni:

  1. Öntsön egy csésze kávét.
  2. Melegíts fel egy serpenyőt, majd süssön két tojást.
  3. Süssön három burgonyatócsnit.
  4. Piríts két kenyeret.
  5. Kenj vajat és lekvárt a pirítósra.
  6. Öntsön egy pohár narancslevet.

Ha jártas a főzésben, ezeket az utasításokat aszinkronban. Elkezdi melegedni a serpenyőt a tojásokhoz, majd elkezdi a hash browns főzését. Tedd a kenyeret a kenyérpirítóba, majd kezdd el főzni a tojásokat. A folyamat minden egyes lépésében elindít egy feladatot, majd továbblép más feladatokra, amelyek készen állnak a figyelmére.

A reggeli főzés jó példa az aszinkron munkára, amely nem párhuzamos. Egy személy (vagy szál) képes kezelni az összes feladatot. Egy személy aszinkron módon reggelizhet úgy, hogy az előző tevékenység befejeződése előtt elindítja a következő feladatot. Minden főzési feladat halad, függetlenül attól, hogy valaki aktívan figyeli-e a folyamatot. Amint elkezdi melegedni a serpenyőt a tojásokhoz, elkezdheti a hash browns főzését. Miután a hash-brownok elkezdenek sülni, a kenyeret a kenyérpirítóba teheti.

Párhuzamos algoritmushoz több olyan személyre van szükség, aki főz (vagy több szálat). Az egyik ember megfőzi a tojásokat, egy másik a hash brownst, és így tovább. Minden személy egy adott feladatra összpontosít. Minden olyan személy, aki főz (vagy minden szál) szinkronban van blokkolva, és várja, hogy a jelenlegi feladat befejeződjön: a hash brownok készen állnak a megfordításra, a kenyér készen áll, hogy kiugorjon a kenyérpirítóból, és így tovább.

Diagram, amely a reggeli elkészítésére vonatkozó utasításokat mutatja be, a hét egymást követő feladat 30 perc alatt elvégzett listájaként.

Vegye figyelembe a C#-kódutasításokkal írt szinkron utasítások listáját:

using System;
using System.Threading.Tasks;

namespace AsyncBreakfast
{
    // These classes are intentionally empty for the purpose of this example. They are simply marker classes for the purpose of demonstration, contain no properties, and serve no other purpose.
    internal class HashBrown { }
    internal class Coffee { }
    internal class Egg { }
    internal class Juice { }
    internal class Toast { }

    class Program
    {
        static void Main(string[] args)
        {
            Coffee cup = PourCoffee();
            Console.WriteLine("coffee is ready");

            Egg eggs = FryEggs(2);
            Console.WriteLine("eggs are ready");

            HashBrown hashBrown = FryHashBrowns(3);
            Console.WriteLine("hash browns are ready");

            Toast toast = ToastBread(2);
            ApplyButter(toast);
            ApplyJam(toast);
            Console.WriteLine("toast is ready");

            Juice oj = PourOJ();
            Console.WriteLine("oj is ready");
            Console.WriteLine("Breakfast is ready!");
        }

        private static Juice PourOJ()
        {
            Console.WriteLine("Pouring orange juice");
            return new Juice();
        }

        private static void ApplyJam(Toast toast) =>
            Console.WriteLine("Putting jam on the toast");

        private static void ApplyButter(Toast toast) =>
            Console.WriteLine("Putting butter on the toast");

        private static Toast ToastBread(int slices)
        {
            for (int slice = 0; slice < slices; slice++)
            {
                Console.WriteLine("Putting a slice of bread in the toaster");
            }
            Console.WriteLine("Start toasting...");
            Task.Delay(3000).Wait();
            Console.WriteLine("Remove toast from toaster");

            return new Toast();
        }

        private static HashBrown FryHashBrowns(int patties)
        {
            Console.WriteLine($"putting {patties} hash brown patties in the pan");
            Console.WriteLine("cooking first side of hash browns...");
            Task.Delay(3000).Wait();
            for (int patty = 0; patty < patties; patty++)
            {
                Console.WriteLine("flipping a hash brown patty");
            }
            Console.WriteLine("cooking the second side of hash browns...");
            Task.Delay(3000).Wait();
            Console.WriteLine("Put hash browns on plate");

            return new HashBrown();
        }

        private static Egg FryEggs(int howMany)
        {
            Console.WriteLine("Warming the egg pan...");
            Task.Delay(3000).Wait();
            Console.WriteLine($"cracking {howMany} eggs");
            Console.WriteLine("cooking the eggs ...");
            Task.Delay(3000).Wait();
            Console.WriteLine("Put eggs on plate");

            return new Egg();
        }

        private static Coffee PourCoffee()
        {
            Console.WriteLine("Pouring coffee");
            return new Coffee();
        }
    }
}

Ha ezeket az utasításokat számítógépként értelmezi, a reggeli körülbelül 30 percet vesz igénybe. Az időtartam az egyes tevékenységidők összege. A számítógép blokkol minden utasítást, amíg az összes munka be nem fejeződik, majd továbblép a következő feladat utasítására. Ez a megközelítés jelentős időt vehet igénybe. A reggeli példában a számítógépes módszer nem kielégítő reggelit hoz létre. A szinkron listában szereplő későbbi feladatok, például a kenyér pirítósa, csak a korábbi feladatok befejeződése után kezdődnek. Néhány étel hideg lesz, mielőtt a reggeli készen áll a kiszolgálásra.

Ha azt szeretné, hogy a számítógép aszinkron módon hajtsa végre az utasításokat, aszinkron kódot kell írnia. Ügyfélprogramok írásakor azt szeretné, hogy a felhasználói felület reagáljon a felhasználói bemenetre. Az alkalmazásnak nem szabad minden interakciót rögzítenie, miközben adatokat tölt le az internetről. Kiszolgálóprogramok írásakor nem szeretné letiltani azokat a szálakat, amelyek más kéréseket is kiszolgálhatnak. A szinkron kód használata, amikor elérhetők aszinkron alternatívák, gyengíti annak lehetőségét, hogy költséghatékonyan skálázhasson. A letiltott szálakért fizetnie kell.

A sikeres modern alkalmazásokhoz aszinkron kód szükséges. Nyelvi támogatás nélkül az aszinkron kód írásához visszahívások, befejezési események vagy más eszközök szükségesek, amelyek elhomályosítják a kód eredeti szándékát. A szinkron kód előnye a lépésenkénti művelet, amely megkönnyíti a beolvasást és a megértést. A hagyományos aszinkron modellek arra kényszerítik, hogy a kód aszinkron jellegére összpontosítson, nem pedig a kód alapvető műveleteire.

Ne blokkoljon, inkább várjon

Az előző kód egy szerencsétlen programozási gyakorlatot emel ki: szinkron kód írása aszinkron műveletek végrehajtásához. A kód megakadályozza, hogy az aktuális szál bármilyen más munkát végez. A kód nem szakítja meg a szálat, miközben futnak a feladatok. Ennek a modellnek az eredménye hasonló ahhoz, mint amikor a kenyérpirítót bámulja, miután betette a kenyeret. Hagyja figyelmen kívül a megszakításokat, és ne kezdjen el más feladatokat, amíg a kenyér fel nem bukkan. Nem veszed ki a vajat és a lekvárt a hűtőből. Előfordulhat, hogy nem veszi észre, hogy tűz keletkezik a tűzhelyen. Egyszerre pirítsd meg a kenyeret, és kezeld a többi problémát is. Ugyanez igaz a kódra is.

Első lépésként frissítse a kódot, hogy a szál ne blokkolja a feladatok végrehajtása alatt. A await kulcsszó lehetővé teszi egy feladat nem blokkoló módon történő elindítását, majd a végrehajtás folytatását a feladat befejezésekor. A reggeli kód egy egyszerű aszinkron verziója a következő kódrészlethez hasonlóan néz ki:

static async Task Main(string[] args)
{
    Coffee cup = PourCoffee();
    Console.WriteLine("coffee is ready");

    Egg eggs = await FryEggsAsync(2);
    Console.WriteLine("eggs are ready");

    HashBrown hashBrown = await FryHashBrownsAsync(3);
    Console.WriteLine("hash browns are ready");

    Toast toast = await ToastBreadAsync(2);
    ApplyButter(toast);
    ApplyJam(toast);
    Console.WriteLine("toast is ready");

    Juice oj = PourOJ();
    Console.WriteLine("oj is ready");
    Console.WriteLine("Breakfast is ready!");
}

A kód frissíti az eredeti metódustörzseket FryEggs, FryHashBrowns, és ToastBread egyaránt úgy, hogy azok rendre Task<Egg>, Task<HashBrown>, és Task<Toast> objektumokat adnak vissza. A frissített metódusnevek közé tartozik az "Async" utótag: FryEggsAsync, FryHashBrownsAsyncés ToastBreadAsync. A Main metódus visszaadja a Task objektumot, de nem rendelkezik return kifejezéssel, amely tervezésből áll. További információ: Void értéket visszaadó aszinkron függvény kiértékelése.

Jegyzet

A frissített kód még nem használja ki az aszinkron programozás legfontosabb funkcióit, ami rövidebb befejezési időt eredményezhet. A kód nagyjából ugyanannyi idő alatt dolgozza fel a feladatokat, mint a kezdeti szinkron verzió. A teljes metódus-implementációkért tekintse meg a kód végleges verzióját, a jelen cikk későbbi részében.

Alkalmazzuk a reggeli példát a frissített kódra. A szál nem lesz blokkolva, miközben a tojások vagy a röszti sül, de a kód nem kezd el más feladatokat, amíg a jelenlegi munka be nem fejeződik. Még mindig tedd a kenyeret a kenyérpirítóba, és bámuld a kenyérpirítót, amíg a kenyér fel nem bukkan, de most már reagálhat a megszakításokra. Egy étteremben, ahol több megrendelést is leadnak, a szakács új megrendelést indíthat, míg egy másik már főz.

A frissített kódban a reggelin dolgozó szál nincs blokkolva, miközben bármely megkezdett, de befejezetlen feladatra vár. Egyes alkalmazások esetében csak erre a módosításra van szükség. Engedélyezheti, hogy az alkalmazás támogassa a felhasználói interakciót, miközben az adatok letöltődnek az internetről. Más helyzetekben érdemes lehet más tevékenységeket elindítani, miközben az előző tevékenység befejezésére vár.

Tevékenységek egyidejű indítása

A legtöbb művelethez több független tevékenységet szeretne azonnal elindítani. Ahogy minden feladat elkészül, megkezdhet más munkákat, amelyek készen állnak a kezdésre. Ha ezt a módszertant alkalmazza a reggelire, gyorsabban elkészítheti a reggelit. Mindent közel ugyanahhoz az időponthoz készít, így élvezheti a forró reggelit.

Az System.Threading.Tasks.Task osztály és a kapcsolódó típusok olyan osztályok, amelyekkel ezt az érvelési stílust alkalmazhatja a folyamatban lévő tevékenységekre. Ez a megközelítés lehetővé teszi, hogy olyan kódot írjon, amely jobban hasonlít arra, ahogyan a valós életben reggelit készít. Elkezded készíteni a tojásokat, a hash brownst és a pirítóst egyszerre. Mivel minden étel figyelmet igényel, figyelmét erre a feladatra fordítja, elvégzi a feladatot, és várakozik valamire, ami újra figyelmet igényel.

A kódban elindít egy feladatot, és megtartja a munkát jelképező Task objektumot. A feladat await metódusával késleltetheti a munka elvégzését, amíg az eredmény el nem készül.

Alkalmazza ezeket a módosításokat a reggeli kódra. Az első lépés az, hogy a műveletek feladatait a kezdéskor tárolják, ahelyett, hogy a await kifejezést használják.

Coffee cup = PourCoffee();
Console.WriteLine("Coffee is ready");

Task<Egg> eggsTask = FryEggsAsync(2);
Egg eggs = await eggsTask;
Console.WriteLine("Eggs are ready");

Task<HashBrown> hashBrownTask = FryHashBrownsAsync(3);
HashBrown hashBrown = await hashBrownTask;
Console.WriteLine("Hash browns are ready");

Task<Toast> toastTask = ToastBreadAsync(2);
Toast toast = await toastTask;
ApplyButter(toast);
ApplyJam(toast);
Console.WriteLine("Toast is ready");

Juice oj = PourOJ();
Console.WriteLine("Oj is ready");
Console.WriteLine("Breakfast is ready!");

Ezek a módosítások nem segítenek abban, hogy gyorsabban elkészüljön a reggeli. A await kifejezést automatikusan minden feladatra alkalmazzák, amint megkezdődnek. A következő lépés a await hash browns és a tojás kifejezéseinek áthelyezése a módszer végére, mielőtt a reggelit szolgáljuk fel:

Coffee cup = PourCoffee();
Console.WriteLine("Coffee is ready");

Task<Egg> eggsTask = FryEggsAsync(2);
Task<HashBrown> hashBrownTask = FryHashBrownsAsync(3);
Task<Toast> toastTask = ToastBreadAsync(2);

Toast toast = await toastTask;
ApplyButter(toast);
ApplyJam(toast);
Console.WriteLine("Toast is ready");
Juice oj = PourOJ();
Console.WriteLine("Oj is ready");

Egg eggs = await eggsTask;
Console.WriteLine("Eggs are ready");
HashBrown hashBrown = await hashBrownTask;
Console.WriteLine("Hash browns are ready");

Console.WriteLine("Breakfast is ready!");

Most már van egy aszinkron módon elkészített reggeli, amely körülbelül 20 percet vesz igénybe a felkészüléshez. A teljes főzési idő csökken, mert egyes feladatok párhuzamosan futnak.

Ábra, amely a reggeli elkészítésére vonatkozó utasításokat nyolc aszinkron feladatként mutatja be, amelyek körülbelül 20 perc alatt befejeződnek, ahol sajnos a tojások és a hash barnák égnek.

A kódfrissítések a főzési idő csökkentésével javítják az előkészítési folyamatot, de regressziót vezetnek be a tojások és a hash barnák elégetésével. Az összes aszinkron feladatot egyszerre indíthatja el. Az egyes tevékenységekre csak akkor kell várnia, ha az eredményekre szüksége van. A kód hasonlíthat egy webalkalmazás programjára, amely különböző mikroszolgáltatásokra irányuló kérelmeket küld, majd egyetlen lapra egyesíti az eredményeket. Az összes kérést azonnal végrehajtja, majd alkalmazza a await kifejezést az összes feladatra, és írja meg a weblapot.

A feladatok összeállításának támogatása

Az előző kódváltozatok segítenek abban, hogy minden egyszerre készüljön fel a reggelire, kivéve a pirítóst. A pirítós készítésének folyamata egy kompozíció egy aszinkron művelet (a kenyér pirítása) és szinkron műveletek (vaj és lekvár kenése a pirítósra) között. Ez a példa egy fontos fogalmat mutat be az aszinkron programozásról:

Fontos

Egy aszinkron művelet, amelyet szinkron munka követ, szintén aszinkron művelet marad. Másként fogalmazva, ha egy művelet bármely része aszinkron, a teljes művelet aszinkron.

Az előző frissítésekben megtanulta, hogyan használhat Task vagy Task<TResult> objektumokat a futó feladatok tárolására. Minden tevékenységre vár, mielőtt felhasználná az eredményét. A következő lépés az, hogy olyan metódusokat hozzon létre, amelyek más munka kombinációját jelölik. A reggeli elfogyasztása előtt meg kell várnia a kenyér pirítását jelképező feladatot, mielőtt a vajat és a lekvárt szétterítené.

Ezt a műveletet a következő kóddal jelölheti:

static async Task<Toast> MakeToastWithButterAndJamAsync(int number)
{
    var toast = await ToastBreadAsync(number);
    ApplyButter(toast);
    ApplyJam(toast);

    return toast;
}

A MakeToastWithButterAndJamAsync metódus aláírásában szerepel a async módosító, amely jelzi a fordítónak, hogy a metódus await kifejezést tartalmaz, és aszinkron műveleteket tartalmaz. A módszer azt a feladatot jelöli, amely pirít a kenyeret, majd szétteríti a vajat és a lekvárt. A metódus egy Task<TResult> objektumot ad vissza, amely a három művelet összetételét jelöli.

A módosított fő kódblokk a következőképpen néz ki:

static async Task Main(string[] args)
{
    Coffee cup = PourCoffee();
    Console.WriteLine("coffee is ready");

    var eggsTask = FryEggsAsync(2);
    var hashBrownTask = FryHashBrownsAsync(3);
    var toastTask = MakeToastWithButterAndJamAsync(2);

    var eggs = await eggsTask;
    Console.WriteLine("eggs are ready");

    var hashBrown = await hashBrownTask;
    Console.WriteLine("hash browns are ready");

    var toast = await toastTask;
    Console.WriteLine("toast is ready");

    Juice oj = PourOJ();
    Console.WriteLine("oj is ready");
    Console.WriteLine("Breakfast is ready!");
}

Ez a kódmódosítás egy fontos technikát mutat be az aszinkron kód használatához. A feladatokat úgy állítjuk össze, hogy a műveleteket egy új metódusba választjuk szét, amely egy feladatot ad vissza. Megadhatja, hogy mikor várjon a feladatra. Egyidejűleg más tevékenységeket is elindíthat.

Aszinkron kivételek kezelése

Eddig a pontig a kód implicit módon feltételezi, hogy az összes tevékenység sikeresen befejeződött. Az aszinkron metódusok kivételeket vetnek fel, ugyanúgy, mint szinkron társaik. A kivételek és a hibakezelés aszinkron támogatásának célja általában megegyezik az aszinkron támogatással. Az ajánlott eljárás a szinkron utasítások sorozataként olvasható kód írása. A tevékenységek kivételeket vetnek ki, ha nem tudnak sikeresen teljesíteni. Az ügyfélkód akkor tudja elkapni ezeket a kivételeket, ha a await kifejezés egy megkezdett tevékenységre van alkalmazva.

A reggeli példában tegyük fel, hogy a kenyérpirító meggyullad a kenyér pirításával. Ezt a problémát szimulálhatja úgy, hogy módosítja a ToastBreadAsync metódust az alábbi kódnak megfelelően:

private static async Task<Toast> ToastBreadAsync(int slices)
{
    for (int slice = 0; slice < slices; slice++)
    {
        Console.WriteLine("Putting a slice of bread in the toaster");
    }
    Console.WriteLine("Start toasting...");
    await Task.Delay(2000);
    Console.WriteLine("Fire! Toast is ruined!");
    throw new InvalidOperationException("The toaster is on fire");
    await Task.Delay(1000);
    Console.WriteLine("Remove toast from toaster");

    return new Toast();
}

Jegyzet

A kód lefordításakor figyelmeztetés jelenik meg a nem elérhető kódról. Ez a hiba terv szerint történik. Miután a kenyérpirító kigyulladt, a műveletek nem megfelelően haladnak, és a kód hibát ad vissza.

A kód módosítása után futtassa az alkalmazást, és ellenőrizze a kimenetet:

Pouring coffee
Coffee is ready
Warming the egg pan...
putting 3 hash brown patties in the pan
Cooking first side of hash browns...
Putting a slice of bread in the toaster
Putting a slice of bread in the toaster
Start toasting...
Fire! Toast is ruined!
Flipping a hash brown patty
Flipping a hash brown patty
Flipping a hash brown patty
Cooking the second side of hash browns...
Cracking 2 eggs
Cooking the eggs ...
Put hash browns on plate
Put eggs on plate
Eggs are ready
Hash browns are ready
Unhandled exception. System.InvalidOperationException: The toaster is on fire
   at AsyncBreakfast.Program.ToastBreadAsync(Int32 slices) in Program.cs:line 65
   at AsyncBreakfast.Program.MakeToastWithButterAndJamAsync(Int32 number) in Program.cs:line 36
   at AsyncBreakfast.Program.Main(String[] args) in Program.cs:line 24
   at AsyncBreakfast.Program.<Main>(String[] args)

Vegye észre, hogy jó néhány feladat végez a kenyérpirító kigyulladása és a rendszer kivétel észlelése között. Ha egy aszinkron módon futó tevékenység kivételt vet ki, akkor a feladat hibás. A Task objektum tartalmazza a Task.Exception tulajdonságban kidobott kivételt. A hibás tevékenységek kivételt képeznek, ha a await kifejezés a tevékenységre van alkalmazva.

Ennek a folyamatnak két fontos mechanizmusa van:

  • A kivétel tárolása hibás feladatban
  • A kivétel kicsomagolásának és újbóli dobásának menete, amikor a kód egy hibás feladaton várakozik (await)

Ha az aszinkron módon futó kód kivételt jelez, a kivételt a rendszer a Task objektumban tárolja. A Task.Exception tulajdonság egy System.AggregateException objektum, mert az aszinkron munka során több kivétel is előfordulhat. A rendszer hozzáad minden kivételt a AggregateException.InnerExceptions gyűjteményhez. Ha a Exception tulajdonság null értékű, egy új AggregateException objektum jön létre, és a kidobott kivétel a gyűjtemény első eleme.

A hibás tevékenységek leggyakoribb forgatókönyve, hogy a Exception tulajdonság pontosan egy kivételt tartalmaz. Ha a kód egy hibás feladatra vár, a gyűjtemény első AggregateException.InnerExceptions kivételét visszadobja. Ez az oka annak, hogy a példában szereplő kimenet egy System.InvalidOperationException objektumot jelenít meg AggregateException objektum helyett. Az első belső kivétel kinyerésével az aszinkron metódusok használata a lehető legnagyobb mértékben hasonlít a szinkron megfelelőikkel való munkavégzéshez. A kódban megvizsgálhatja a Exception tulajdonságot, ha a forgatókönyv több kivételt is generálhat.

Borravaló

Az ajánlott eljárás az, hogy az argumentum-érvényesítési kivételek szinkron módon a feladatvisszaadó metódusokból. További információkért és példákért lásd a Kivételek a feladat-visszaküldési metódusokbanrészt.

Mielőtt továbblép a következő szakaszra, fűzzön megjegyzést a következő két utasításhoz a ToastBreadAsync metódusban. Nem szeretne újabb tüzet gyújtani:

Console.WriteLine("Fire! Toast is ruined!");
throw new InvalidOperationException("The toaster is on fire");

Várva várt kifejezések alkalmazása a tevékenységekre hatékonyan

Az await osztály metódusaival javíthatja az előző kód végén található Task kifejezések sorozatát. Az egyik API a WhenAll metódus, amely egy Task objektumot ad vissza, amely akkor fejeződik be, ha az argumentumlistájában szereplő összes tevékenység befejeződött. A következő kód ezt a módszert mutatja be:

await Task.WhenAll(eggsTask, hashBrownTask, toastTask);
Console.WriteLine("Eggs are ready");
Console.WriteLine("Hash browns are ready");
Console.WriteLine("Toast is ready");
Console.WriteLine("Breakfast is ready!");

Egy másik lehetőség a WhenAny metódus használata, amely egy Task<Task> objektumot ad vissza, amely akkor fejeződik be, amikor bármelyik argumentuma befejeződik. Megvárhatja a visszaadott feladatot, mert tudja, hogy a feladat elkészült. Az alábbi kód bemutatja, hogyan használhatja a WhenAny metódust az első feladat befejezésére, majd az eredmény feldolgozására. Miután feldolgozta a befejezett tevékenység eredményét, eltávolítja az elvégzett tevékenységet a WhenAny metódusnak átadott tevékenységek listájából.

var breakfastTasks = new List<Task> { eggsTask, hashBrownTask, toastTask };
while (breakfastTasks.Count > 0)
{
    Task finishedTask = await Task.WhenAny(breakfastTasks);
    if (finishedTask == eggsTask)
    {
        Console.WriteLine("Eggs are ready");
    }
    else if (finishedTask == hashBrownTask)
    {
        Console.WriteLine("Hash browns are ready");
    }
    else if (finishedTask == toastTask)
    {
        Console.WriteLine("Toast is ready");
    }
    await finishedTask;
    breakfastTasks.Remove(finishedTask);
}

A kódrészlet vége közelében figyelje meg a await finishedTask; kifejezést. Ez a sor azért fontos, mert Task.WhenAny a Task<Task> befejezett tevékenységet tartalmazó burkolófeladatot ad vissza. Amikor a await Task.WhenAnyburkoló tevékenység befejezésére vár, az eredmény pedig az elsőként befejezett tényleges tevékenység. A tevékenység eredményének lekéréséhez vagy a kivételek megfelelőségének biztosításához azonban magának a befejezett tevékenységnek kell await lennie (a fájlban finishedTasktárolva). Annak ellenére, hogy tudja, hogy a tevékenység befejeződött, az ismételt várakozás lehetővé teszi, hogy hozzáférjen az eredményéhez, vagy kezelje az esetlegesen a hibát okozó kivételeket.

A végleges kód áttekintése

Így néz ki a kód végleges verziója:

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace AsyncBreakfast
{
    // These classes are intentionally empty for the purpose of this example. They are simply marker classes for the purpose of demonstration, contain no properties, and serve no other purpose.
    internal class HashBrown { }
    internal class Coffee { }
    internal class Egg { }
    internal class Juice { }
    internal class Toast { }

    class Program
    {
        static async Task Main(string[] args)
        {
            Coffee cup = PourCoffee();
            Console.WriteLine("coffee is ready");

            var eggsTask = FryEggsAsync(2);
            var hashBrownTask = FryHashBrownsAsync(3);
            var toastTask = MakeToastWithButterAndJamAsync(2);

            var breakfastTasks = new List<Task> { eggsTask, hashBrownTask, toastTask };
            while (breakfastTasks.Count > 0)
            {
                Task finishedTask = await Task.WhenAny(breakfastTasks);
                if (finishedTask == eggsTask)
                {
                    Console.WriteLine("eggs are ready");
                }
                else if (finishedTask == hashBrownTask)
                {
                    Console.WriteLine("hash browns are ready");
                }
                else if (finishedTask == toastTask)
                {
                    Console.WriteLine("toast is ready");
                }
                await finishedTask;
                breakfastTasks.Remove(finishedTask);
            }

            Juice oj = PourOJ();
            Console.WriteLine("oj is ready");
            Console.WriteLine("Breakfast is ready!");
        }

        static async Task<Toast> MakeToastWithButterAndJamAsync(int number)
        {
            var toast = await ToastBreadAsync(number);
            ApplyButter(toast);
            ApplyJam(toast);

            return toast;
        }

        private static Juice PourOJ()
        {
            Console.WriteLine("Pouring orange juice");
            return new Juice();
        }

        private static void ApplyJam(Toast toast) =>
            Console.WriteLine("Putting jam on the toast");

        private static void ApplyButter(Toast toast) =>
            Console.WriteLine("Putting butter on the toast");

        private static async Task<Toast> ToastBreadAsync(int slices)
        {
            for (int slice = 0; slice < slices; slice++)
            {
                Console.WriteLine("Putting a slice of bread in the toaster");
            }
            Console.WriteLine("Start toasting...");
            await Task.Delay(3000);
            Console.WriteLine("Remove toast from toaster");

            return new Toast();
        }

        private static async Task<HashBrown> FryHashBrownsAsync(int patties)
        {
            Console.WriteLine($"putting {patties} hash brown patties in the pan");
            Console.WriteLine("cooking first side of hash browns...");
            await Task.Delay(3000);
            for (int patty = 0; patty < patties; patty++)
            {
                Console.WriteLine("flipping a hash brown patty");
            }
            Console.WriteLine("cooking the second side of hash browns...");
            await Task.Delay(3000);
            Console.WriteLine("Put hash browns on plate");

            return new HashBrown();
        }

        private static async Task<Egg> FryEggsAsync(int howMany)
        {
            Console.WriteLine("Warming the egg pan...");
            await Task.Delay(3000);
            Console.WriteLine($"cracking {howMany} eggs");
            Console.WriteLine("cooking the eggs ...");
            await Task.Delay(3000);
            Console.WriteLine("Put eggs on plate");

            return new Egg();
        }

        private static Coffee PourCoffee()
        {
            Console.WriteLine("Pouring coffee");
            return new Coffee();
        }
    }
}

A kód körülbelül 15 perc alatt elvégzi az aszinkron reggelizési feladatokat. A teljes idő csökken, mert egyes tevékenységek párhuzamosan futnak. A kód egyszerre több tevékenységet figyel, és csak szükség szerint hajt végre műveleteket.

diagram, amely a reggeli előkészítésére vonatkozó utasításokat mutatja hat aszinkron feladatként, amelyek körülbelül 15 perc alatt befejeződnek, és a kód figyeli az esetleges megszakításokat.

A végső kód aszinkron. Pontosabban tükrözi, hogy egy személy hogyan főzhet reggelit. Hasonlítsa össze a végső kódot a cikkben szereplő első kódmintával. Az alapvető műveletek továbbra is egyértelműek a kód elolvasásával. A végső kódot ugyanúgy olvashatja el, mint a reggeli készítésére vonatkozó utasításokat, ahogy a cikk elején látható. A async és await kulcsszavak nyelvi funkciói megkönnyítik mindenki számára a fordítást az írott utasítások követéséhez: Indítsa el a feladatokat, amikor csak lehet, és ne akadjon el, miközben a feladatok befejezésére vár.

Async/await kontra ContinueWith

A async és await kulcsszavak szintaktikai egyszerűsítést nyújtanak a Task.ContinueWith közvetlen használatával szemben. Bár async, /, await és ContinueWith hasonló szemantikával rendelkeznek az aszinkron műveletek kezeléséhez, a fordító nem feltétlenül fordítja le a await kifejezéseket közvetlenül ContinueWith metódushívásokra. Ehelyett a fordító optimalizált állapotgép-kódot hoz létre, amely ugyanazt a logikai viselkedést biztosítja. Ez az átalakítás jelentős olvashatósági és karbantarthatósági előnyöket biztosít, különösen több aszinkron művelet láncolása esetén.

Fontolja meg azt a forgatókönyvet, amelyben több szekvenciális aszinkron műveletet kell végrehajtania. A következőképpen néz ki ugyanaz a logika, amikor ContinueWith-vel van megvalósítva, összehasonlítva async/await-mal.

A ContinueWith használata

Az ContinueWithaszinkron műveletek sorozatának minden lépéséhez beágyazott folytatások szükségesek:

// Using ContinueWith - demonstrates the complexity when chaining operations
static Task MakeBreakfastWithContinueWith()
{
    return StartCookingEggsAsync()
        .ContinueWith(eggsTask =>
        {
            var eggs = eggsTask.Result;
            Console.WriteLine("Eggs ready, starting bacon...");
            return StartCookingBaconAsync();
        })
        .Unwrap()
        .ContinueWith(baconTask =>
        {
            var bacon = baconTask.Result;
            Console.WriteLine("Bacon ready, starting toast...");
            return StartToastingBreadAsync();
        })
        .Unwrap()
        .ContinueWith(toastTask =>
        {
            var toast = toastTask.Result;
            Console.WriteLine("Toast ready, applying butter...");
            return ApplyButterAsync(toast);
        })
        .Unwrap()
        .ContinueWith(butteredToastTask =>
        {
            var butteredToast = butteredToastTask.Result;
            Console.WriteLine("Butter applied, applying jam...");
            return ApplyJamAsync(butteredToast);
        })
        .Unwrap()
        .ContinueWith(finalToastTask =>
        {
            var finalToast = finalToastTask.Result;
            Console.WriteLine("Breakfast completed with ContinueWith!");
        });
}

Az async/await használata

Ugyanaz a műveletsor async/await használata sokkal természetesebben olvasható.

// Using async/await - much cleaner and easier to read
static async Task MakeBreakfastWithAsyncAwait()
{
    var eggs = await StartCookingEggsAsync();
    Console.WriteLine("Eggs ready, starting bacon...");
    
    var bacon = await StartCookingBaconAsync();
    Console.WriteLine("Bacon ready, starting toast...");
    
    var toast = await StartToastingBreadAsync();
    Console.WriteLine("Toast ready, applying butter...");
    
    var butteredToast = await ApplyButterAsync(toast);
    Console.WriteLine("Butter applied, applying jam...");
    
    var finalToast = await ApplyJamAsync(butteredToast);
    Console.WriteLine("Breakfast completed with async/await!");
}

Miért előnyben részesítjük az aszinkron/várakozási módot?

A async/await megközelítés számos előnnyel jár:

  • Olvashatóság: A kód úgy olvasható, mint a szinkron kód, így könnyebben megérthető a műveletek folyamata.
  • Karbantarthatóság: A sorrendben szereplő lépések hozzáadásához vagy eltávolításához minimális kódmódosításra van szükség.
  • Hibakezelés: A blokkok try/catch kivételkezelése természetesen működik, míg ContinueWith a hibás feladatok gondos kezelését igényli.
  • Hibakeresés: A hívásverem és a hibakeresői élmény sokkal jobb a async/await.
  • Teljesítmény: A fordító optimalizációi async/await fejlettebbek, mint a manuális ContinueWith láncolatok.

Az előny még nyilvánvalóbbá válik a láncolt műveletek számának növekedésével. Bár egyetlen folytatás kezelhető ContinueWith, a 3–4 vagy több aszinkron művelet sorozatai gyorsan nehezen olvashatók és karbantarthatók. Ez a funkcionális programozásban "monadikus do-jelölés" néven ismert minta lehetővé teszi, hogy több aszinkron műveletet szekvenciálisan és olvasható módon komponáljon.

Következő lépés