Programowanie asynchroniczne przy użyciu elementów async i await

Asynchroniczny model programowania zadań (TAP) zapewnia abstrakcję kodu asynchronicznego. Kod jest pisany jako sekwencja instrukcji, podobnie jak zawsze. Ten kod można odczytać tak, jakby każda instrukcja kończyła się przed następnym rozpoczęciem. Kompilator wykonuje wiele przekształceń, ponieważ niektóre z tych instrukcji mogą rozpocząć pracę i zwrócić wartość Task reprezentującą bieżącą pracę.

Jest to cel tej składni: włącz kod, który odczytuje jak sekwencję instrukcji, ale jest wykonywany w znacznie bardziej skomplikowanej kolejności na podstawie alokacji zasobów zewnętrznych i po zakończeniu zadań. Jest to analogiczne do sposobu, w jaki ludzie dają instrukcje dotyczące procesów, które obejmują zadania asynchroniczne. W tym artykule użyjesz przykładowych instrukcji dotyczących podejmowania decyzji, aby zobaczyć, jak słowa kluczowe i ułatwiają uzasadnienie kodu, który zawiera serię instrukcji async await asynchronicznych. Aby wyjaśnić, jak zrobić coś, co chcesz zrobić, napisz instrukcje podobne do poniższej listy:

  1. Posypać kubek kawy.
  2. Podgrzać rąbek, a następnie rozgrzać dwie kęsy.
  3. Trzy wycinki bekonu.
  4. Toast two pieces of bread (Podsuchaj dwa rodzaje bread).
  5. Dodaj cygę i jam do wyskakującego powiadomienia.
  6. Nawiąż kielicha pomarańczowego sosu.

Jeśli masz doświadczenie w pracy z programem , należy wykonać te instrukcje asynchronicznie. Należy zacząć rozgrzewkę dla wody, a następnie uruchomić boczek. W tosterze umieścisz więc bułę, a następnie zaczniesz ćgę. Na każdym etapie procesu należy rozpocząć zadanie, a następnie zwrócić uwagę na zadania gotowe do uwagi.

To dobry przykład pracy asynchronicznej, która nie jest równoległa. Jedna osoba (lub wątek) może obsłużyć wszystkie te zadania. Kontynuując analogię do infekcyjną, jedna osoba może zrobić asynchronicznie, uruchamiać następne zadanie przed ukończeniem pierwszego. Postęp filmu będzie się rozwijać niezależnie od tego, czy ktoś go obserwuje. Od razu, gdy zaczniesz rozgrzewkę dla miski, możesz rozpocząć rozgrzewkę boczek. Po zakończeniu bekonu możesz włożyć do tosteru bułę.

W przypadku algorytmu równoległego potrzebujesz wielu kuchaczy (lub wątków). Jeden z nich to jeden z bekonów i tak dalej. Każdy z nich koncentruje się tylko na tym jednym zadaniu. Każdy kuchar (lub wątek) zostałby zablokowany synchronicznie, czekając, aż bacon będzie gotowy do przerzucania, lub wyskakujące okienko do okienka podręcznego.

Teraz rozważ te same instrukcje zapisane jako instrukcje języka 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();
        }
    }
}

synchroniczny

Synchronicznie przygotowany nasyp, trwał około 30 minut, ponieważ suma jest sumą poszczególnych zadań.

Uwaga

Klasy Coffee , , , i są Egg Bacon Toast Juice puste. Są one po prostu klasami znaczników na potrzeby pokazu, nie zawierają żadnych właściwości i nie służą żadnemu iniekcjom.

Komputery nie interpretują tych instrukcji tak samo, jak robią to ludzie. Komputer będzie blokować każdą z instrukcji do momentu zakończenia pracy przed przejściem do następnej instrukcji. Powoduje to niezadowolenie. Późniejsze zadania nie zostaną uruchomione, dopóki wcześniejsze zadania nie zostaną ukończone. Utworzenie zapasu trwałoby znacznie dłużej, a niektóre elementy zostałyby zimne przed podaniem.

Jeśli chcesz, aby komputer wykonywał powyższe instrukcje asynchronicznie, musisz napisać kod asynchroniczny.

Te kwestie są ważne dla programów, które piszesz dzisiaj. Podczas pisania programów klienckich chcesz, aby interfejs użytkownika reagował na dane wejściowe użytkownika. Aplikacja nie powinna sprawić, że telefon będzie wyświetlany jako zamrożony podczas pobierania danych z Internetu. Podczas pisania programów serwerowych nie chcesz blokować wątków. Te wątki mogą służyć do obsługi innych żądań. Użycie kodu synchronicznego, gdy istnieją asynchroniczne alternatywy, nie ma możliwości tańszego skalowania w zewnątrz. Płacisz za te zablokowane wątki.

Udane nowoczesne aplikacje wymagają kodu asynchronicznego. Bez obsługi języka pisanie kodu asynchronicznego wymagało wywołań zwrotnych, zdarzeń ukończenia lub innych środków, które przesłaniały pierwotną intencję kodu. Zaletą kodu synchronicznego jest to, że jego działania krok po kroku ułatwiają skanowanie i zrozumienie. Tradycyjne modele asynchroniczne wymuszały skupienie się na asynchronicznym charakterze kodu, a nie na podstawowych działaniach kodu.

Nie blokuj, await zamiast tego

Poprzedni kod demonstruje złe rozwiązanie: konstruowanie kodu synchronicznego w celu wykonywania operacji asynchronicznych. Zgodnie z tym, co zostało zapisane, ten kod blokuje wykonywanie przez wątek wykonywania jakiejkolwiek innej pracy. Nie zostanie on przerwany, gdy żadne z zadań jest w toku. Wyglądałoby to tak, jakby po wrzuceniu do tostera wchłonił się ser. Zignorujesz wszystkie osoby, które rozmawiają z Tobem, aż do wyskakującego powiadomienia.

Zacznijmy od zaktualizowania tego kodu, aby wątek nie blokował się podczas wykonywania zadań. Słowo kluczowe zapewnia nieblokowanie sposobu uruchamiania zadania, a następnie kontynuowania await wykonywania po zakończeniu tego zadania. Prosta asynchroniczna wersja kodu sprawia, że kod będzie wyglądać podobnie do następującego fragmentu kodu:

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

Ważne

Łączny czas, który upłynął, jest w przybliżeniu taki sam jak początkowa wersja synchroniczna. Kod nie musi jeszcze korzystać z niektórych kluczowych funkcji programowania asynchronicznego.

Porada

Wszystkie treści metod obiektów , i zostały zaktualizowane w celu zwrócenia FryEggsAsync FryBaconAsync odpowiednio , i ToastBreadAsync Task<Egg> Task<Bacon> Task<Toast> . Nazwy metod zostały zmienione z oryginalnej wersji na sufiks "Async". Ich implementacje są wyświetlane jako część ostatniej wersji w dalszej części tego artykułu.

Ten kod nie blokuje podczas chłoniaka ani boconu. Ten kod nie uruchamia jednak żadnych innych zadań. Nadal możesz umieścić wyskakujące okienko w wyskakującym okienku i zaglądać do niego, aż pojawi się. Ale co najmniej, odpowiadasz wszystkim, którzy chcą Twoją uwagę. W restauracji, w której składanych jest wiele zamówień, kucharka może rozpocząć kolejną obiad, gdy pierwszy jest w stanie schłonić.

Teraz wątek pracujący nad schowekem nie jest blokowany podczas czekania na wszystkie uruchomione zadania, które jeszcze nie zostały ukończone. W przypadku niektórych aplikacji ta zmiana jest potrzebna. Aplikacja z graficznym interfejsem użytkownika nadal odpowiada użytkownikowi tylko za pomocą tej zmiany. Jednak w tym scenariuszu potrzebujesz więcej. Nie chcesz, aby każde zadanie składnika było wykonywane sekwencyjnie. Lepiej jest uruchomić każde z zadań składowych przed oczekiwaniem na ukończenie poprzedniego zadania.

Współbieżne uruchamianie zadań

W wielu scenariuszach chcesz natychmiast uruchomić kilka niezależnych zadań. Następnie, po zakończeniu każdego zadania, możesz kontynuować inną pracę, która jest gotowa. W analogicznej formie można szybciej zrobić to w ten sposób. Wszystkie te informacje są wykonywane blisko tego samego czasu. Otrzymasz gorącą nagotę.

Typy System.Threading.Tasks.Task powiązane i to klasy, których można użyć do wytłaniania informacji o zadaniach, które są w toku. Dzięki temu można napisać kod, który bardziej przypomina sposób, w jaki faktycznie tworzy się łańcowice. W tym samym czasie zaczyna się od cyferki, bekonu i wyskakującego powiadomienia. Ponieważ każdy z nich wymaga wykonania akcji, należy zwrócić uwagę na to zadanie, zająć się następną akcją, a następnie poczekać na coś innego, co wymaga Twojej uwagi.

Uruchamiasz zadanie i przytrzymując na Task obiekcie reprezentującym pracę. Przed rozpoczęciem pracy z jego wynikiem będziesz await mieć do wykonania każde zadanie.

Dokonajmy tych zmian w kodzie źródłowym. Pierwszym krokiem jest przechowywanie zadań dla operacji podczas ich uruchamiania, a nie oczekiwanie na nie:

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

Następnie można przenieść instrukcje dla baconu i na koniec metody await przed dodaniem do nich instrukcji :

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

asynchroniczne asynchroniczne

Asynchronicznie przygotowana operacja trwała około 20 minut. Tym razem oszczędza się, ponieważ niektóre zadania są uruchamiane współbieżnie.

Powyższy kod działa lepiej. Wszystkie zadania asynchroniczne są uruchamiane jednocześnie. Każde zadanie należy czekać tylko wtedy, gdy będą potrzebne wyniki. Poprzedni kod może być podobny do kodu w aplikacji internetowej, który wykonuje żądania różnych mikrousług, a następnie łączy wyniki w jedną stronę. Natychmiast wyślisz wszystkie żądania, a następnie wszystkie await te zadania i skomponuj stronę internetową.

Kompozycja z zadaniami

Wszystko jest gotowe do najechań w tym samym czasie z wyjątkiem wyskakującego powiadomienia. Tworzenie wyskakującego powiadomienia to kompozycja operacji asynchronicznej (wyskakującej nasypki z powiadomieniami) i operacji synchronicznych (dodawanie chłoniaka i zacięcie). Aktualizacja tego kodu ilustruje ważną koncepcję:

Ważne

Kompozycja operacji asynchronicznej, po której następuje praca synchroniczna, jest operacją asynchroniczną. Określono inny sposób: jeśli jakakolwiek część operacji jest asynchroniczna, cała operacja jest asynchroniczna.

Powyższy kod pokazuje, że można używać obiektów Task lub Task<TResult> do przechowywania uruchomionych zadań. Każde await zadanie należy przed użyciem jego wyniku. Następnym krokiem jest utworzenie metod reprezentujących kombinację innych prac. Przed rozpoczęciem obsługi zjedz, chcesz poczekać na zadanie, które reprezentuje toasting bread przed dodaniem dań i zacięć. Można to reprezentować za pomocą następującego kodu:

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

    return toast;
}

Poprzednia metoda ma async w sygnaturze modyfikator . Sygnalizuje to kompilatorowi, że ta metoda zawiera await instrukcje ; zawiera operacje asynchroniczne. Ta metoda reprezentuje zadanie, które polega na tym, aby podsycać bułę, a następnie dodaje cykadę i zacięcie. Ta metoda zwraca Task<TResult> wartość , która reprezentuje kompozycję tych trzech operacji. Głównym blokiem kodu jest teraz:

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

Poprzednia zmiana ilustrowała ważną technikę pracy z kodem asynchronicznym. Zadania można skojregować, oddzielając operacje na nową metodę, która zwraca zadanie. Możesz wybrać, kiedy należy czekać na to zadanie. Inne zadania można uruchamiać współbieżnie.

Wyjątki asynchroniczne

Do tego momentu niejawnie przyjęto założenie, że wszystkie te zadania zostały ukończone pomyślnie. Metody asynchroniczne zgłaszają wyjątki, podobnie jak ich synchroniczne odpowiedniki. Asynchroniczna obsługa wyjątków i błędów dąży do tych samych celów co obsługa asynchroniczna ogólnie: należy napisać kod, który odczytuje jak serię instrukcji synchronicznych. Zadania zrzucają wyjątki, gdy nie mogą zakończyć się pomyślnie. Kod klienta może przechwyć te wyjątki, gdy uruchomione zadanie to awaited . Załóżmy na przykład, że toster wyzpala się podczas tworzenia wyskakującego powiadomienia. Można to zasymulować, ToastBreadAsync modyfikując metodę tak, aby dopasować ją 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();
}

Uwaga

Podczas kompilowania poprzedniego kodu otrzymasz ostrzeżenie dotyczące nieosiąganego kodu. Jest to zamierzone, ponieważ po pożarze tostera operacje nie będą kontynuowane normalnie.

Uruchom aplikację po w związku z wprowadzeniem tych zmian, a dane wyjściowe będą podobne do następującego tekstu:

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 między pożarem tostera a zaobserwowaniem wyjątku jest wykonywanych sporo zadań. Gdy zadanie, które jest uruchamiane asynchronicznie, zgłasza wyjątek, to zadanie ma błąd. Obiekt Task zawiera wyjątek zgłoszony we właściwości Task.Exception . Zadania uszkodzone zgłaszają wyjątek, gdy są oczekujące.

Istnieją dwa ważne mechanizmy, które należy zrozumieć: sposób przechowywania wyjątku w uszkodzonym zadaniu oraz sposób rozpakowywania i ponownego pakowania wyjątku, gdy kod oczekuje na uszkodzone zadanie.

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

Najbardziej powszechnym scenariuszem dla błędnych zadań jest to, że Exception właściwość zawiera dokładnie jeden wyjątek. Gdy awaits koduje zadanie o błędach, pierwszy wyjątek w kolekcji AggregateException.InnerExceptions jest ponownie cytowany. Dlatego dane wyjściowe z tego przykładu pokazują element InvalidOperationException zamiast AggregateException . Wyodrębnienie pierwszego wyjątku wewnętrznego sprawia, że praca z metodami asynchronicznmi jest jak najbardziej podobna do pracy z ich synchronicznymi odpowiednikami. Właściwość w kodzie Exception można sprawdzić, gdy scenariusz może generować wiele wyjątków.

Przed rozpoczęciem wyeksmentuj te dwa wiersze w ToastBreadAsync metodzie jako komentarz. Nie chcesz uruchamiać kolejnego pożaru:

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

Wydajne oczekiwanie na zadania

Serię instrukcji na końcu poprzedniego kodu można ulepszyć przy użyciu await metod Task klasy . Jeden z tych interfejsów API to , który zwraca wartość , która kończy się po ukończeniu wszystkich zadań na liście argumentów, jak pokazano w WhenAll Task poniższym kodzie:

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 funkcji , która zwraca wartość , która kończy się po WhenAny Task<Task> zakończeniu któregokolwiek z jego argumentów. Możesz poczekać na zwrócone zadanie, wiedząc, że zostało ono już zakończone. Poniższy kod pokazuje, jak można użyć funkcji , aby poczekać na zakończenie pierwszego zadania, WhenAny a następnie przetworzyć jego wynik. Po przetworzeniu wyniku ukończonego zadania należy usunąć to ukończone zadanie z listy zadań przekazanych 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 tych wszystkich zmianach ostateczna wersja kodu wygląda następująco:

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

gdy dowolna asynchroniczna synchronizacja

Końcowa wersja asynchronicznie przygotowanego nasypu zajęła około 15 minut, ponieważ niektóre zadania były uruchamiane współbieżnie, a kod monitorował wiele zadań jednocześnie i podjęł działania tylko wtedy, gdy było to potrzebne.

Ten końcowy kod jest asynchroniczny. Dokładniej odzwierciedla to sposób, w jaki osoba będzie jękać najgorętszą osobę. Porównaj poprzedni kod z pierwszym przykładowym kodem w tym artykule. Podstawowe akcje nadal wyraźnie odczytują kod. Możesz przeczytać ten kod w taki sam sposób, w jaki przeczytasz te instrukcje na początku tego artykułu. Funkcje językowe funkcji języka i zapewniają tłumaczenie wykonywane przez każdą osobę w celu wykonania tych napisanych instrukcji: rozpoczynanie zadań zgodnie z możliwościami i nie blokują oczekiwania na async await ukończenie zadań.

Następne kroki