Udostępnij za pośrednictwem


Programowanie asynchroniczne z użyciem async i await

Model programowania asynchronicznego TAP (zadanie ) stanowi warstwę abstrakcji nad typowym kodowaniem asynchronicznym. W tym modelu napiszesz kod jako sekwencję instrukcji, tak samo jak zwykle. Różnica polega na tym, że można odczytać kod oparty na zadaniach podczas, gdy kompilator przetwarza każdą instrukcję, zanim rozpocznie przetwarzanie następnej instrukcji. W celu wykonania tego modelu kompilator wykonuje wiele przekształceń w celu wykonania każdego zadania. Niektóre instrukcje mogą inicjować pracę i zwracać obiekt Task, który reprezentuje bieżącą pracę, a kompilator musi rozwiązać te przekształcenia. Celem programowania asynchronicznego zadań jest umożliwienie kodu, który wygląda jak sekwencja instrukcji, ale wykonuje się w bardziej skomplikowanej kolejności. Wykonywanie jest oparte na alokacji zasobów zewnętrznych i po zakończeniu zadań.

Model programowania asynchronicznego zadania jest analogiczny do sposobu udzielania instrukcji dotyczących procesów obejmujących zadania asynchroniczne. W tym artykule użyto przykładu z instrukcjami dotyczącymi tworzenia śniadania, aby pokazać, w jaki sposób async i await słowa kluczowe ułatwiają wnioskowanie o kodzie zawierającym serię instrukcji asynchronicznych. Instrukcje dotyczące robienia śniadania mogą być podane jako lista:

  1. Wlać filiżankę kawy.
  2. Rozgrzej patelnię, a następnie smażyj dwa jaja.
  3. Usmaż trzy plasterki boczku.
  4. Podpiecz dwa kawałki chleba.
  5. Rozłóż masło i dżem na tosty.
  6. Wlać szklankę soku pomarańczowego.

Jeśli masz doświadczenie z gotowaniem, możesz wykonać te instrukcje asynchronicznie. Zaczynasz rozgrzewać patelnię na jaja, a następnie zacząć smażyć boczek. Umieścisz chleb w tosterze, a następnie zaczniesz gotować jaja. Na każdym etapie procesu uruchamiasz zadanie, a następnie przechodzisz do innych zadań, które są gotowe do uwagi.

Gotowanie śniadania jest dobrym przykładem asynchronicznej pracy, która nie jest równoległa. Jedna osoba (lub wątek) może obsłużyć wszystkie zadania. Jedna osoba może wykonać śniadanie asynchronicznie, uruchamiając następne zadanie przed ukończeniem poprzedniego zadania. Każde zadanie gotowania postępuje niezależnie od tego, czy ktoś aktywnie obserwuje ten proces. Jak tylko zaczniesz rozgrzewać patelnię na jaja, możesz rozpocząć smażenie boczku. Po tym, jak boczek zaczyna gotować, można umieścić chleb w tosterze.

W przypadku algorytmu równoległego potrzebujesz wielu osób, które gotują (lub wiele wątków). Jedna osoba gotuje jaja, inna smaży boczek, i tak dalej. Każda osoba koncentruje się na jednym konkretnym zadaniu. Każda osoba, która gotuje (lub każdy wątek) jest blokowana synchronicznie, czekając na ukończenie bieżącego zadania: Boczek gotowy do przewracania, chleb gotowy do wyskoczenia z tostera itd.

Diagram przedstawiający instrukcje dotyczące przygotowywania śniadania jako listy siedmiu kolejnych zadań zakończonych w ciągu 30 minut.

Rozważ tę samą listę synchronicznych instrukcji napisanych jako instrukcje kodu języka C#:

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 Bacon { }
    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");

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

Jeśli zinterpretujesz te instrukcje tak, jak zrobiłby to komputer, co oznacza, że śniadanie potrwa około 30 minut. Czas trwania to suma poszczególnych czasów zadań. Komputer blokuje każdą instrukcję, dopóki wszystkie prace nie zostaną zakończone, a następnie przejdzie do następnej instrukcji zadania. Takie podejście może zająć dużo czasu. W przykładzie śniadaniowym metoda komputerowa tworzy niezadowolające śniadanie. Późniejsze zadania na liście synchronicznej, takie jak tostowanie chleba, nie zaczynają się, dopóki wcześniejsze zadania nie zostaną ukończone. Niektóre potrawy są zimne, zanim śniadanie jest gotowe do serwowania.

Jeśli chcesz, aby komputer wykonywał instrukcje asynchronicznie, musisz napisać kod asynchroniczny. Podczas pisania programów klienckich interfejs użytkownika ma odpowiadać na dane wejściowe użytkownika. Aplikacja nie powinna blokować całej interakcji podczas pobierania danych z Internetu. Podczas pisania programów serwera nie chcesz blokować wątków, które mogą obsługiwać inne żądania. Użycie kodu synchronicznego, gdy istnieją asynchroniczne alternatywy, szkodzi możliwości skalowania w poziomie tańszego. Płacisz za zablokowane wątki.

Udane nowoczesne aplikacje wymagają kodu asynchronicznego. Bez obsługi języka pisanie kodu asynchronicznego wymaga wywołań zwrotnych, zdarzeń ukończenia lub innych środków, które ukrywają oryginalną intencję kodu. Zaletą synchronicznego kodu jest akcja krok po kroku, która ułatwia skanowanie i zrozumienie. Tradycyjne modele asynchroniczne wymuszają skupienie się na asynchronicznym charakterze kodu, a nie na podstawowych działaniach kodu.

Nie blokuj, czekaj zamiast tego

Poprzedni kod wyróżnia niefortunne rozwiązanie programistyczne: pisanie synchronicznego kodu w celu wykonywania operacji asynchronicznych. Kod blokuje bieżący wątek przed wykonywaniem jakichkolwiek innych czynności. Kod nie przerywa wątku podczas wykonywania zadań. Wynik tego modelu jest podobny do wpatrywania się w toster po włożeniu chleba. Ignorujesz wszelkie przerwy i nie uruchamiasz innych zadań, dopóki chleb nie pojawi się. Nie zabierasz masła i dżem z lodówki. Możesz nie zauważyć, gdy na kuchence zacznie się palić. Chcesz zarówno opiekasz chleb, jak i zająć się innymi kwestiami jednocześnie. To samo dotyczy kodu.

Możesz zacząć od zaktualizowania kodu, aby wątek nie blokował się podczas wykonywania zadań. Słowo kluczowe await zapewnia nieblokujący sposób uruchamiania zadania, a następnie kontynuowania wykonywania po zakończeniu zadania. Prosta asynchroniczna wersja kodu dotyczącego przygotowywania śniadania wygląda jak następujący fragment:

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!");
}

Kod aktualizuje oryginalne ciała metod FryEggs, FryBacon i ToastBread w taki sposób, aby zwracały odpowiednio obiekty Task<Egg>, Task<Bacon> i Task<Toast>. Zaktualizowane nazwy metod obejmują sufiks "Async": FryEggsAsync, FryBaconAsynci ToastBreadAsync. Metoda Main zwraca obiekt Task, chociaż nie ma wyrażenia return, które jest zgodnie z projektem. Aby uzyskać więcej informacji, zobacz Ocena funkcji asynchronicznej zwracającej void.

Notatka

Zaktualizowany kod nie wykorzystuje jeszcze kluczowych funkcji programowania asynchronicznego, co może spowodować skrócenie czasu ukończenia. Kod przetwarza zadania w mniej więcej tym samym czasie co początkowa wersja synchroniczna. Aby zapoznać się z pełnymi implementacjami metod, zobacz ostateczną wersję kodu w dalszej części tego artykułu.

Zastosujmy przykład śniadania do zaktualizowanego kodu. Wątek nie blokuje się, gdy jaja lub boczek są gotowane, ale kod również nie uruchamia innych zadań, dopóki bieżąca praca nie zostanie ukończona. Nadal kładziesz chleb w tosterze i patrzysz na toster, aż chleb wyskoczy, ale teraz możesz reagować na przerwy. W restauracji, w której składa się wiele zamówień, kucharz może rozpocząć nowe zamówienie, podczas gdy inny już gotuje.

W zaktualizowanym kodzie wątek pracujący nad śniadaniem nie jest blokowany, gdy czeka na zakończenie dowolnego rozpoczętego zadania. W przypadku niektórych aplikacji ta zmiana jest wszystkim, czego potrzebujesz. Możesz umożliwić aplikacji obsługę interakcji użytkownika podczas pobierania danych z Internetu. W innych scenariuszach możesz chcieć uruchomić inne zadania podczas oczekiwania na ukończenie poprzedniego zadania.

Współbieżne uruchamianie zadań

W przypadku większości operacji chcesz natychmiast uruchomić kilka niezależnych zadań. Po zakończeniu każdego zadania inicjujesz inną pracę, która jest gotowa do uruchomienia. Jeśli zastosujesz tę metodologię do przykładu śniadania, możesz szybciej przygotować śniadanie. Wszystko jest gotowe blisko tego samego czasu, dzięki czemu można zjeść gorące śniadanie.

Klasa System.Threading.Tasks.Task i powiązane typy to klasy, których można użyć do zastosowania tego stylu rozumowania do zadań, które są w toku. Takie podejście umożliwia pisanie kodu, który bardziej przypomina sposób tworzenia śniadania w prawdziwym życiu. Zaczynasz gotować jaja, boczek i tosty w tym samym czasie. Ponieważ każdy produkt spożywczy wymaga działania, zwracasz uwagę na to zadanie, zajmujesz się nim, a następnie czekasz na coś innego, co wymaga Twojej uwagi.

W kodzie uruchamiasz zadanie i trzymasz obiekt Task, który reprezentuje pracę. Użyj metody await w zadaniu, aby opóźnić działanie nad pracą do momentu, gdy wynik będzie gotowy.

Zastosuj te zmiany do kodu śniadaniowego. Pierwszym krokiem jest przechowywanie zadań dla operacji podczas ich uruchamiania, zamiast używania wyrażenia await:

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!");

Te poprawki nie pomagają przygotować śniadania szybciej. Wyrażenie await jest stosowane do wszystkich zadań natychmiast po ich uruchomieniu. Następnym krokiem jest przeniesienie await wyrażeń dla boczku i jajek na koniec metody, zanim podasz śniadanie.

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!");

Masz teraz asynchronicznie przygotowane śniadanie, które trwa około 20 minut. Całkowity czas gotowania jest zmniejszony, ponieważ niektóre zadania są uruchamiane współbieżnie.

Diagram, który pokazuje instrukcje przygotowania śniadania jako osiem zadań asynchronicznych, które zakończą się w około 20 minut, gdzie niestety, jaja i boczek spalić.

Aktualizacje kodu usprawniają proces przygotowywania, skracając czas gotowania, ale wprowadzają regresję poprzez spalanie jaj i boczku. Wszystkie zadania asynchroniczne są uruchamiane jednocześnie. Zaczekasz na każde zadanie tylko wtedy, gdy będą potrzebne wyniki. Kod może być podobny do programu w aplikacji internetowej, który wysyła żądania do różnych mikrousług, a następnie łączy wyniki w jedną stronę. Natychmiast wysyłasz wszystkie żądania, następnie stosujesz wyrażenie await do wszystkich tych zadań i składasz stronę internetową.

Obsługa kompozycji z zadaniami

Poprzednie modyfikacje kodu pomagają przygotować wszystko na śniadanie w tym samym czasie, z wyjątkiem tostów. Proces tworzenia tosty jest kompozycji operacji asynchronicznej (tosty chleba) z synchronicznych operacji (rozprzestrzenianie masła i dżem na tosty). W tym przykładzie przedstawiono ważne pojęcie dotyczące programowania asynchronicznego:

Ważny

Kompozycja operacji asynchronicznej z synchroniczną pracą stanowi operację asynchroniczną. Określono inny sposób, jeśli jakakolwiek część operacji jest asynchroniczna, cała operacja jest asynchroniczna.

W poprzednich aktualizacjach pokazano, jak używać obiektów Task lub Task<TResult> do przechowywania uruchomionych zadań. Należy poczekać na zakończenie każdego zadania, zanim użyje się jego wyniku. Następnym krokiem jest utworzenie metod reprezentujących kombinację innej pracy. Zanim podasz śniadanie, poczekaj na przygotowanie tostów z chleba przed rozsmarowaniem masła i dżemu.

Tę pracę można przedstawić przy użyciu następującego kodu:

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

    return toast;
}

Metoda MakeToastWithButterAndJamAsync zawiera modyfikator async w podpisie, który sygnalizuje kompilatorowi, że metoda zawiera wyrażenie await i zawiera operacje asynchroniczne. Metoda reprezentuje zadanie, które najpierw tostuje chleb, a potem smaruje masłem i dżemem. Metoda zwraca obiekt Task<TResult>, który reprezentuje kompozycję trzech operacji.

Poprawiony główny blok kodu wygląda teraz następująco:

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!");
}

Ta zmiana kodu ilustruje ważną technikę pracy z kodem asynchronicznym. Zadania można tworzyć, oddzielając operacje na nową metodę zwracającą zadanie. Możesz wybrać, kiedy zaczekać na to zadanie. Inne zadania można uruchamiać jednocześnie.

Obsługa wyjątków asynchronicznych

Do tego momentu kod niejawnie zakłada, że wszystkie zadania zostały ukończone pomyślnie. Metody asynchroniczne zgłaszają wyjątki, podobnie jak ich synchroniczne odpowiedniki. Cele wsparcia asynchronicznego dla wyjątków i obsługi błędów są takie same jak w przypadku wsparcia asynchronicznego w ogóle. Najlepszym rozwiązaniem jest napisanie kodu, który odczytuje się jak seria instrukcji synchronicznych. Zadania wyrzucają wyjątki, gdy nie mogą zakończyć się pomyślnie. Kod klienta może przechwytywać te wyjątki, gdy wyrażenie await jest stosowane do uruchomionego zadania.

W przykładzie śniadaniowym załóżmy, że toster zapala się podczas tostowania chleba. Możesz zasymulować ten problem, modyfikując metodę ToastBreadAsync w celu dopasowania do następującego kodu:

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

Notatka

Po skompilowaniu tego kodu zostanie wyświetlone ostrzeżenie o nieosiągalnym kodzie. Ten błąd jest zamierzony. Po tym jak toster się zapali, operacje nie przebiegają normalnie, a kod zwraca błąd.

Po wprowadzeniu zmian kodu uruchom aplikację i sprawdź dane wyjściowe:

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)

Zwróć uwagę, że sporo zadań kończy się między czasem, gdy toster się zapali, a system zaobserwuje wyjątek. Gdy zadanie uruchamiane asynchronicznie zgłasza wyjątek, to zadanie kończy się niepowodzeniem. Obiekt Task zawiera wyjątek zgłoszony we właściwości Task.Exception. Zadania obciążone błędem zgłaszają wyjątek, gdy wyrażenie await jest zastosowane do zadania.

Istnieją dwa ważne mechanizmy umożliwiające zrozumienie tego procesu:

  • Jak wyjątek jest przechowywany w uszkodzonym zadaniu
  • Jak wyjątek jest rozpakowany i ponownie wprowadzany, gdy kod czeka (await) na uszkodzonym zadaniu

Gdy kod uruchomiony asynchronicznie zgłasza wyjątek, wyjątek jest przechowywany w obiekcie Task. Właściwość Task.Exception jest obiektem System.AggregateException, ponieważ podczas pracy asynchronicznej może zostać zgłoszony więcej niż jeden wyjątek. Każdy zgłoszony wyjątek jest dodawany do kolekcji AggregateException.InnerExceptions. Jeśli właściwość Exception ma wartość null, zostanie utworzony nowy obiekt AggregateException, a zgłoszony wyjątek jest pierwszym elementem w kolekcji.

Najbardziej typowym scenariuszem dla błędnego zadania jest to, że właściwość Exception zawiera dokładnie jeden wyjątek. Kiedy twój kod czeka na zadaniu z błędem, ponownie zgłasza pierwszy wyjątek AggregateException.InnerExceptions w kolekcji. Ten wynik jest powodem, dla którego dane wyjściowe z przykładu pokazują obiekt System.InvalidOperationException, a nie obiekt AggregateException. Wyodrębnienie pierwszego wyjątku wewnętrznego sprawia, że praca z metodami asynchronicznymi jest jak najbardziej podobna do pracy z ich synchronicznymi odpowiednikami. Właściwość Exception można sprawdzić w kodzie, gdy scenariusz może wygenerować wiele wyjątków.

Napiwek

Zalecaną praktyką jest, aby wszelkie wyjątki weryfikacji argumentów pojawiły się synchronicznie z metod zwracania zadań. Aby uzyskać więcej informacji i przykładów, zobacz Wyjątki w metodach zwracania zadań.

Zanim przejdziesz do następnej sekcji, wykomentuj następujące dwie instrukcje w swojej metodzie ToastBreadAsync. Nie chcesz uruchamiać innego ognia:

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

Efektywne stosowanie wyrażeń await do zadań

Serię wyrażeń await można poprawić na końcu poprzedniego kodu, używając metod klasy Task. Jednym z interfejsów API jest metoda WhenAll, która zwraca obiekt Task, kończący się, gdy wszystkie zadania na liście argumentów są zakończone. Poniższy kod przedstawia tę metodę:

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!");

Inną opcją jest użycie metody WhenAny, która zwraca obiekt Task<Task> zakończony po zakończeniu dowolnego z jego argumentów. Możesz poczekać na zwrócone zadanie, ponieważ wiadomo, że zadanie zostało wykonane. Poniższy kod pokazuje, jak można użyć metody WhenAny, aby zaczekać na zakończenie pierwszego zadania, a następnie przetworzyć jego wynik. Po przetworzeniu wyniku z ukończonego zadania należy usunąć ukończone zadanie z listy zadań przekazanych do metody 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");
    }
    await finishedTask;
    breakfastTasks.Remove(finishedTask);
}

W pobliżu końca fragmentu kodu zwróć uwagę na wyrażenie await finishedTask;. Wyrażenie await Task.WhenAny nie czeka na ukończone zadanie, ale raczej czeka na obiekt Task zwrócony przez metodę Task.WhenAny. Wynikiem metody Task.WhenAny jest ukończone (lub wadliwe) zadanie. Najlepszym rozwiązaniem jest ponowne oczekiwanie na zadanie, nawet jeśli wiadomo, że zadanie zostało ukończone. W ten sposób można pobrać wynik zadania lub dopilnować, aby został zgłoszony wyjątek, który powoduje błąd zadania.

Przejrzyj końcowy kod

Oto jak wygląda ostateczna wersja kodu:

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

Kod wykonuje asynchroniczne zadania śniadaniowe w ciągu około 15 minut. Całkowity czas jest zmniejszany, ponieważ niektóre zadania są uruchamiane współbieżnie. Kod jednocześnie monitoruje wiele zadań i podejmuje działania tylko w razie potrzeby.

Diagram przedstawiający instrukcje przygotowywania śniadania jako sześć zadań asynchronicznych, które są wykonywane w ciągu około 15 minut, a kod monitoruje możliwe przerwy.

Końcowy kod jest asynchroniczny. Dokładniej odzwierciedla to, jak osoba może gotować śniadanie. Porównaj końcowy kod z pierwszym przykładem kodu w artykule. Podstawowe akcje są nadal jasne, odczytując kod. Możesz przeczytać końcowy kod w taki sam sposób, w jaki czytasz listę instrukcji dotyczących robienia śniadania, jak pokazano na początku artykułu. Funkcje związane z słowami kluczowymi async i await umożliwiają tłumaczenie, które każda osoba może wykorzystać do postępowania zgodnie z pisemnymi instrukcjami: Rozpoczynaj zadania natychmiast, kiedy to możliwe, i nie blokuj się podczas oczekiwania na ich zakończenie.

Następny krok