Asynchronní programování pomocí modifikátoru Async a operátoru Await

Model TAP (Task Asynchronous Programming Model) poskytuje abstrakci asynchronního kódu. Kód píšete jako posloupnost příkazů, stejně jako vždy. Tento kód si můžete přečíst, jako by se každý příkaz dokončil před dalším zahájením. Kompilátor provádí mnoho transformací, protože některé z těchto příkazů mohou začít pracovat a vracet objekt , který představuje Task probíhající práci.

To je cílem této syntaxe: povolte kód, který čte jako posloupnost příkazů, ale provádí se v mnohem složitějším pořadí na základě přidělování externích prostředků a po dokončení úkolů. Je obdobou toho, jak lidé poskytují pokyny pro procesy, které zahrnují asynchronní úlohy. V tomto článku použijete příklad instrukcí pro vytvoření mimy, abyste viděli, jak klíčová slova a usnadňují rozhodování o kódu, který obsahuje řadu async await asynchronních instrukcí. Napsali byste pokyny podobné následujícímu seznamu, abyste vysvětlili, jak si posnídat:

  1. Zasytí šálek kávy.
  2. Zahřejme si pan a pak dva mihoty.
  3. Tři řezy v násecích
  4. Informační zprávy o dvou chůdách.
  5. Přidejte k informačnímu signálu písní a jam.
  6. Nasytíte sklenku pomerančového džusu.

Pokud máte zkušenosti s vařením, provedete tyto instrukce asynchronně. Měli byste začít zahřát misku pro sněžku a pak začít s náplní. Vložili byste ho do chůdy a pak byste začali s touhou. V každém kroku procesu spustíte úkol a pak se obrátíte na úkoly, které jsou připravené na vaši pozornost.

Dobrým příkladem asynchronní práce, která není paralelní, je vaření. Všechny tyto úlohy může zpracovávat jedna osoba (nebo vlákno). V analogii se 1 člověk může asynchronně stát tím, že před dokončením prvního úkolu začne další úkol. Průběh postupuje bez ohledu na to, jestli ho někdo sleduje. Jakmile začnete zahřát misku na misku, můžete začít s náplní. Jakmile se začnou chýše, můžete ho dát do šátku.

Pro paralelní algoritmus byste potřebovali více kuchařů (nebo vláken). Jedním z nich by byl keř, jeden a tak dále. Každý z nich by se zaměřoval pouze na tento jeden úkol. Každý kuchař (nebo vlákno) by byl synchronně blokovaný a čekal na to, až bude připravený na překlopení, nebo informační zprávy k popu.

Teď vezměte v úvahu stejné instrukce napsané jako příkazy jazyka C#:

using System;
using System.Threading.Tasks;

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

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

            Bacon bacon = FryBacon(3);
            Console.WriteLine("bacon is 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 Bacon FryBacon(int slices)
        {
            Console.WriteLine($"putting {slices} slices of bacon in the pan");
            Console.WriteLine("cooking first side of bacon...");
            Task.Delay(3000).Wait();
            for (int slice = 0; slice < slices; slice++)
            {
                Console.WriteLine("flipping a slice of bacon");
            }
            Console.WriteLine("cooking the second side of bacon...");
            Task.Delay(3000).Wait();
            Console.WriteLine("Put bacon on plate");

            return new Bacon();
        }

        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();
        }
    }
}

synchronní synchronní režim

Synchronně připravená jídla trvala přibližně 30 minut, protože celkový součet je součet každého jednotlivého úkolu.

Poznámka

Třídy Coffee , , , a jsou Egg Bacon Toast Juice prázdné. Jsou to jednoduše třídy značek pro účely ukázky, neobsahují žádné vlastnosti a nemají žádný jiný účel.

Počítače tyto instrukce ne interpretují stejně jako lidé. Počítač bude blokovat každý příkaz, dokud se práce nedokoní, než se přesune k dalšímu příkazu. Tím se vytvoří nespokojený den. Pozdější úlohy by se nes zahájily, dokud se nedokončily předchozí úlohy. Vytvoření jídla by trvat mnohem déle a některé položky by se před obsluhou zachladí.

Pokud chcete, aby počítač svěřl výše uvedené instrukce asynchronně, musíte napsat asynchronní kód.

Tyto obavy jsou důležité pro programy, které dnes píšete. Při psaní klientských programů chcete, aby uživatelské rozhraní reagovalo na uživatelský vstup. Při stahování dat z webu by vaše aplikace neměla telefon vypadat jako zamrzlý. Při psaní serverových programů nechcete vlákna blokovat. Tato vlákna mohou obsluhut jiné požadavky. Použití synchronního kódu, když existují asynchronní alternativy, poškodí vaši schopnost škálovat na více než nákladnější. Za tato blokovaná vlákna platíte vy.

Úspěšné moderní aplikace vyžadují asynchronní kód. Bez podpory jazyka se při psaní asynchronního kódu vyžadovala zpětná volání, události dokončení nebo jiné prostředky, které zakrýly původní záměr kódu. Výhodou synchronního kódu je, že jeho podrobné akce usnadňuje kontrolu a pochopení. Tradiční asynchronní modely vás přinutil zaměřit se na asynchronní povahu kódu, nikoli na základní akce kódu.

Neblokujte místo toho await.

Předchozí kód ukazuje špatný postup: vytvoření synchronního kódu pro provádění asynchronních operací. Jak je zapsáno, tento kód blokuje vlákno, které ho spouští, od jakékoli jiné práce. Během probíhajících úloh se nepřeruší. Bylo by to, jako byste po vložení chůdy zíslili na chůdu. Ignorovali byste každého, kdo s vámi mluví, dokud se nevypnul informační zprávy.

Za chvíli tento kód aktualizujeme, aby vlákno neblokuje spuštěné úlohy. Klíčové await slovo poskytuje neblokující způsob, jak spustit úlohu, a po dokončení této úlohy pokračovat v provádění. Jednoduchá asynchronní verze kódu make-the-code by vypadala jako následující fragment kódu:

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

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

    Bacon bacon = await FryBaconAsync(3);
    Console.WriteLine("bacon is 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!");
}

Důležité

Celková uplynulá doba je přibližně stejná jako počáteční synchronní verze. Kód ještě nezískal některé klíčové funkce asynchronního programování.

Tip

Těla metod , a byla aktualizována tak, aby vracely FryEggsAsync , a v uvedeném FryBaconAsync ToastBreadAsync Task<Egg> Task<Bacon> Task<Toast> pořadí. Metody se přejmenují z původní verze tak, aby zahrnovaly příponu "Async". Jejich implementace se zobrazují jako součást konečné verze dále v tomto článku.

Tento kód se neblokuje, zatímco se chýlí a hroudí. Tento kód ale nespustí žádné jiné úlohy. Informační zprávy byste pořád vložili do kávy a získaly na něj, dokud se nevysypí. Alespoň byste ale reagovali na všechny, kdo chtěli vaši pozornost. V restauraci, kde se objednává více objednávek, by mohl kuchař spustit další jídlo, zatímco první jídlo nachystá.

Vlákno, které pracuje na návěsce, teď není blokované, když čekáte na spuštěnou úlohu, která ještě nebyla dokončena. U některých aplikací je tato změna vše, co je potřeba. Aplikace s grafickým uživatelským rozhraním stále reaguje právě touto změnou uživateli. V tomto scénáři ale chcete více. Nechcete, aby se všechny úlohy komponent spouštěly postupně. Před čekáním na dokončení předchozího úkolu je lepší spustit všechny úlohy komponent.

Souběžné spuštění úloh

V mnoha scénářích chcete spustit několik nezávislých úloh okamžitě. Po dokončení každého úkolu můžete pokračovat v další práci, která je připravená. V analogii se to dělá rychleji. Vše se také provádí ve stejnou dobu. Dostanete horkou čokoládu.

Související System.Threading.Tasks.Task typy a jsou třídy, které můžete použít k odůvodnění probíhajících úloh. To vám umožní psát kód, který se více podobá způsobu, jakým byste ve skutečnosti vytvářely restaurace. Ve stejnou dobu byste začali s hroudou, hroudou a toastem. Protože každá z nich vyžaduje akci, obrátíte pozornost na tento úkol, postaráte se o další akci a pak čekáte na něco jiného, co vyžaduje vaši pozornost.

Spustíte úlohu a přidržíte Task objekt, který představuje práci. Každý úkol await budete mít před tím, než začnete pracovat s jeho výsledkem.

Pojďme tyto změny provést v kódu kesudu. Prvním krokem je uložení úloh pro operace při jejich spuštění, nikoli jejich čekání:

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

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

Task<Bacon> baconTask = FryBaconAsync(3);
Bacon bacon = await baconTask;
Console.WriteLine("Bacon is 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!");

V dalším kroku můžete před obsluhuí metody přesunout příkazy pro náušní a dech na konec await metody:

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

Task<Egg> eggsTask = FryEggsAsync(2);
Task<Bacon> baconTask = FryBaconAsync(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");
Bacon bacon = await baconTask;
Console.WriteLine("Bacon is ready");

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

asynchronous – asynchronní operace

Asynchronně připravená jídla trvala přibližně 20 minut, tato úspora času je proto, že některé úlohy běžely souběžně.

Předchozí kód funguje lépe. Všechny asynchronní úlohy spustíte najednou. Každý úkol čekáte jenom v případě, že potřebujete výsledky. Předchozí kód může být podobný kódu ve webové aplikaci, který vytváří požadavky různých mikroslužeb a pak kombinuje výsledky do jedné stránky. Všechny požadavky vyžádáte okamžitě, pak await všechny tyto úlohy a seskládat webovou stránku.

Složení s úkoly

Máte všechno připravené k snídani ve stejnou dobu s výjimkou informačních zpráv. Zpřístupnění informačních zpráv je složením asynchronní operace (informační zpráva o příchodu) a synchronní operace (přidání másla a zaseknutí). Aktualizace tohoto kódu ilustruje důležitý koncept:

Důležité

Složení asynchronní operace následované synchronní prací je asynchronní operace. Pokud je libovolná část operace asynchronní, je celá operace asynchronní, pokud je uvedena jinak.

Předchozí kód vám ukázal, že můžete použít Task objekty nebo Task<TResult> pro udržení spuštěných úloh. awaitKaždý úkol před použitím jeho výsledku. Dalším krokem je vytvoření metod, které reprezentují kombinaci jiné práce. Před tím, než zachováte snídani, chcete čekat na úkol, který představuje informační zprávu, před přidáním másla a zaseknutím. Tuto práci můžete vyjádřit pomocí následujícího kódu:

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

    return toast;
}

Předchozí metoda má async v podpisu modifikátor. To signalizuje kompilátoru, že tato metoda obsahuje await příkaz, obsahuje asynchronní operace. Tato metoda představuje úkol, který označuje chléb a pak přidá máslo a zaseknutí. Tato metoda vrátí hodnotu Task<TResult> , která představuje složení těchto tří operací. Hlavní blok kódu teď bude:

static async Task Main(string[] args)
{
    Coffee cup = PourCoffee();
    Console.WriteLine("coffee is ready");
    
    var eggsTask = FryEggsAsync(2);
    var baconTask = FryBaconAsync(3);
    var toastTask = MakeToastWithButterAndJamAsync(2);

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

    var bacon = await baconTask;
    Console.WriteLine("bacon is ready");

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

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

Předchozí změna ukázala důležitou techniku pro práci s asynchronním kódem. Můžete vytvářet úkoly oddělením operací s novou metodou, která vrací úlohu. Můžete vybrat, kdy se má tento úkol očekávat. Můžete spustit souběžně jiné úkoly.

Asynchronní výjimky

Do tohoto okamžiku jste implicitně předpokládali, že všechny tyto úlohy byly úspěšně dokončeny. Asynchronní metody vyvolávají výjimky, stejně jako jejich synchronní protějšky. Asynchronní podpora pro výjimky a zpracování chyb se snaží ke stejným cílům jako asynchronní podpora obecně: měli byste napsat kód, který se přečte jako série synchronních příkazů. Úkoly vyvolávají výjimky, když se nemůžou úspěšně dokončit. Kód klienta může zachytit tyto výjimky, pokud je spuštěný úkol awaited . Předpokládejme například, že informační zpráva se při vytváření informačního střediska zachytí. To můžete simulovat úpravou ToastBreadAsync metody tak, aby odpovídala následujícímu kódu:

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();
}

Poznámka

Když kompilujete předchozí kód týkající se nedosažitelného kódu, zobrazí se upozornění. To je úmyslné, protože jakmile informační zpráva ztratí požár, operace se za normálních okolností neuskuteční.

Spusťte aplikaci po provedení těchto změn a budete mít výstup podobný následujícímu textu:

Pouring coffee
Coffee is ready
Warming the egg pan...
putting 3 slices of bacon in the pan
Cooking first side of bacon...
Putting a slice of bread in the toaster
Putting a slice of bread in the toaster
Start toasting...
Fire! Toast is ruined!
Flipping a slice of bacon
Flipping a slice of bacon
Flipping a slice of bacon
Cooking the second side of bacon...
Cracking 2 eggs
Cooking the eggs ...
Put bacon on plate
Put eggs on plate
Eggs are ready
Bacon is 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)

Všimněte si, že mezi okamžiky, kdy informační zprávu zachytává a je zjištěna výjimka, existuje několik úloh. Když úloha, která spouští asynchronně, vyvolá výjimku, dojde k chybě této úlohy. Objekt Task obsahuje výjimku vyvolanou ve Task.Exception Vlastnosti. Chybové úlohy vyvolávají výjimku, pokud jsou očekávány.

Existují dva důležité mechanismy, jak pochopit: jak je výjimka uložená v úloze s chybou a jak je výjimka nebalena a znovu vyvolána, když kód čeká na chybovou úlohu.

Pokud kód, který spouští asynchronně, vyvolá výjimku, je tato výjimka uložena v Task . Task.ExceptionVlastnost je, System.AggregateException protože během asynchronní práce může být vyvolána více než jedna výjimka. Do kolekce se přidá jakákoli vyvolaná výjimka AggregateException.InnerExceptions . Pokud Exception je tato vlastnost null, vytvoří se nový AggregateException a vyvolaná výjimka je první položka v kolekci.

Nejběžnějším scénářem chybové úlohy je, že Exception vlastnost obsahuje přesně jednu výjimku. Při výskytu awaits chybné úlohy je znovu vyvolána první výjimka v AggregateException.InnerExceptions kolekci. To je důvod, proč výstup z tohoto příkladu zobrazuje InvalidOperationException místo AggregateException . Extrakce první vnitřní výjimky umožňuje pracovat s asynchronními metodami, jak je to možné, při práci s jejich synchronními protějšky. Můžete prozkoumávat Exception vlastnost v kódu, když váš scénář může generovat více výjimek.

Než začnete pracovat, odkomentujte tyto dva řádky v ToastBreadAsync metodě. Nechcete spustit další požár:

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

Pro úlohy čekají efektivně

Řadu await příkazů na konci předchozího kódu lze zlepšit pomocí metod Task třídy. Jedno z těchto rozhraní API je WhenAll , což vrátí, Task která se dokončí po dokončení všech úkolů v seznamu argumentů, jak je znázorněno v následujícím kódu:

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

Další možností je použít WhenAny , který vrátí a Task<Task> , který se dokončí po dokončení některého z jeho argumentů. Můžete očekávat vrácenou úlohu s vědomím, že již byla dokončena. Následující kód ukazuje, jak můžete použít WhenAny k čekání na dokončení první úlohy a následnému zpracování výsledku. Po zpracování výsledku z dokončené úlohy odstraníte tuto dokončenou úlohu ze seznamu úkolů předaných do WhenAny .

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

Po všech změnách bude finální verze kódu vypadat takto:

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

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

            var eggsTask = FryEggsAsync(2);
            var baconTask = FryBaconAsync(3);
            var toastTask = MakeToastWithButterAndJamAsync(2);

            var breakfastTasks = new List<Task> { eggsTask, baconTask, toastTask };
            while (breakfastTasks.Count > 0)
            {
                Task finishedTask = await Task.WhenAny(breakfastTasks);
                if (finishedTask == eggsTask)
                {
                    Console.WriteLine("eggs are ready");
                }
                else if (finishedTask == baconTask)
                {
                    Console.WriteLine("bacon is ready");
                }
                else if (finishedTask == toastTask)
                {
                    Console.WriteLine("toast is ready");
                }
                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<Bacon> FryBaconAsync(int slices)
        {
            Console.WriteLine($"putting {slices} slices of bacon in the pan");
            Console.WriteLine("cooking first side of bacon...");
            await Task.Delay(3000);
            for (int slice = 0; slice < slices; slice++)
            {
                Console.WriteLine("flipping a slice of bacon");
            }
            Console.WriteLine("cooking the second side of bacon...");
            await Task.Delay(3000);
            Console.WriteLine("Put bacon on plate");

            return new Bacon();
        }

        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();
        }
    }
}

Když jakákoli asynchronní snídaně

Konečná verze asynchronně připravené snídani trvala zhruba 15 minut, protože některé úlohy běžely souběžně a kód sleduje více úkolů najednou a v době, kdy byl potřeba, se v případě potřeby vykonala akce.

Tento konečný kód je asynchronní. Přesněji odráží, jak by osoba navařené snídani. Porovnejte předchozí kód s první ukázkou kódu v tomto článku. Základní akce jsou stále jasné z čtení kódu. Tento kód si můžete přečíst stejným způsobem, jakým jste si přečetli tyto pokyny pro vytvoření snídaně na začátku tohoto článku. Jazykové funkce pro async a await poskytují překlad pro všechny uživatele, kteří se dodávají podle těchto písemných pokynů: spustit úlohy jako vy a neblokovat čekání na dokončení úkolů.

Další kroky