Udostępnij za pomocą


Programowanie asynchroniczne z funkcją Async i Await (Visual Basic)

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. Smaż trzy placki ziemniaczane.
  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 jajka, a następnie zaczynasz gotować placki ziemniaczane. 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. Gdy tylko zaczniesz rozgrzewać patelnię na jajka, możesz rozpocząć gotowanie placków ziemniaczanych. Gdy placki ziemniaczane zaczynają się smażyć, można włożyć chleb do tostera.

W przypadku algorytmu równoległego potrzebujesz wielu osób, które gotują (lub wiele wątków). Jedna osoba gotuje jaja, inny gotuje hash browns, 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: placki ziemniaczane gotowe do odwrócenia, chleb gotowy do wyjęcia 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ę instrukcji synchronicznych napisanych jako instrukcje kodu języka Visual Basic:

Sub Main()
    Dim cup As Coffee = PourCoffee()
    Console.WriteLine("coffee is ready")

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

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

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

    Dim oj As Juice = PourOJ()
    Console.WriteLine("oj is ready")
    Console.WriteLine("Breakfast is ready!")
End Sub

Private Function PourOJ() As Juice
    Console.WriteLine("Pouring orange juice")
    Return New Juice()
End Function

Private Sub ApplyJam(toast As Toast)
    Console.WriteLine("Putting jam on the toast")
End Sub

Private Sub ApplyButter(toast As Toast)
    Console.WriteLine("Putting butter on the toast")
End Sub

Private Function ToastBread(slices As Integer) As Toast
    For slice As Integer = 0 To slices - 1
        Console.WriteLine("Putting a slice of bread in the toaster")
    Next
    Console.WriteLine("Start toasting...")
    Task.Delay(3000).Wait()
    Console.WriteLine("Remove toast from toaster")

    Return New Toast()
End Function

Private Function FryHashBrowns(patties As Integer) As HashBrown
    Console.WriteLine($"putting {patties} hash brown patties in the pan")
    Console.WriteLine("cooking first side of hash browns...")
    Task.Delay(3000).Wait()
    For patty As Integer = 0 To patties - 1
        Console.WriteLine("flipping a hash brown patty")
    Next
    Console.WriteLine("cooking the second side of hash browns...")
    Task.Delay(3000).Wait()
    Console.WriteLine("Put hash browns on plate")

    Return New HashBrown()
End Function

Private Function FryEggs(howMany As Integer) As Egg
    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()
End Function

Private Function PourCoffee() As Coffee
    Console.WriteLine("Pouring coffee")
    Return New Coffee()
End Function

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:

Module AsyncBreakfastProgram
    Async Function Main() As Task
        Dim cup As Coffee = PourCoffee()
        Console.WriteLine("coffee is ready")

        Dim eggs As Egg = Await FryEggsAsync(2)
        Console.WriteLine("eggs are ready")

        Dim hashBrown As HashBrown = Await FryHashBrownsAsync(3)
        Console.WriteLine("hash browns are ready")

        Dim toast As Toast = Await ToastBreadAsync(2)
        ApplyButter(toast)
        ApplyJam(toast)
        Console.WriteLine("toast is ready")

        Dim oj As Juice = PourOJ()
        Console.WriteLine("oj is ready")
        Console.WriteLine("Breakfast is ready!")
    End Function

    Private Async Function ToastBreadAsync(slices As Integer) As Task(Of Toast)
        For slice As Integer = 0 To slices - 1
            Console.WriteLine("Putting a slice of bread in the toaster")
        Next
        Console.WriteLine("Start toasting...")
        Await Task.Delay(3000)
        Console.WriteLine("Remove toast from toaster")

        Return New Toast()
    End Function

    Private Async Function FryHashBrownsAsync(patties As Integer) As Task(Of HashBrown)
        Console.WriteLine($"putting {patties} hash brown patties in the pan")
        Console.WriteLine("cooking first side of hash browns...")
        Await Task.Delay(3000)
        For patty As Integer = 0 To patties - 1
            Console.WriteLine("flipping a hash brown patty")
        Next
        Console.WriteLine("cooking the second side of hash browns...")
        Await Task.Delay(3000)
        Console.WriteLine("Put hash browns on plate")

        Return New HashBrown()
    End Function

    Private Async Function FryEggsAsync(howMany As Integer) As Task(Of Egg)
        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()
    End Function

    Private Function PourCoffee() As Coffee
        Console.WriteLine("Pouring coffee")
        Return New Coffee()
    End Function

    Private Function PourOJ() As Juice
        Console.WriteLine("Pouring orange juice")
        Return New Juice()
    End Function

    Private Sub ApplyJam(toast As Toast)
        Console.WriteLine("Putting jam on the toast")
    End Sub

    Private Sub ApplyButter(toast As Toast)
        Console.WriteLine("Putting butter on the toast")
    End Sub
End Module

Kod aktualizuje oryginalne ciała metod FryEggs, FryHashBrowns i ToastBread w taki sposób, aby zwracały odpowiednio obiekty Task(Of Egg), Task(Of HashBrown) i Task(Of Toast). Zaktualizowane nazwy metod obejmują sufiks "Async": FryEggsAsync, FryHashBrownsAsynci ToastBreadAsync. Funkcja Main zwraca Task obiekt, chociaż nie ma Return wyrażenia, co jest zamierzone.

Uwaga / 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. Pełne implementacje metod można znaleźć w końcowej wersji kodu w dalszej części tego artykułu.

Zastosujmy przykład śniadania do zaktualizowanego kodu. Wątek nie blokuje się, podczas gdy jajka lub placki ziemniaczane są smażone, ale kod nie rozpoczyna 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 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ć jajka, placki ziemniaczane 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:

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

Dim eggsTask As Task(Of Egg) = FryEggsAsync(2)
Dim eggs As Egg = Await eggsTask
Console.WriteLine("Eggs are ready")

Dim hashBrownTask As Task(Of HashBrown) = FryHashBrownsAsync(3)
Dim hashBrown As HashBrown = Await hashBrownTask
Console.WriteLine("Hash browns are ready")

Dim toastTask As Task(Of Toast) = ToastBreadAsync(2)
Dim toast As Toast = Await toastTask
ApplyButter(toast)
ApplyJam(toast)
Console.WriteLine("Toast is ready")

Dim oj As Juice = 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 wyrażeń dotyczących hash browns i jajek na koniec metody, zanim podasz śniadanie.

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

Dim eggsTask As Task(Of Egg) = FryEggsAsync(2)
Dim hashBrownTask As Task(Of HashBrown) = FryHashBrownsAsync(3)
Dim toastTask As Task(Of Toast) = ToastBreadAsync(2)

Dim toast As Toast = Await toastTask
ApplyButter(toast)
ApplyJam(toast)
Console.WriteLine("Toast is ready")
Dim oj As Juice = PourOJ()
Console.WriteLine("Oj is ready")

Dim eggs As Egg = Await eggsTask
Console.WriteLine("Eggs are ready")
Dim hashBrown As HashBrown = Await hashBrownTask
Console.WriteLine("Hash browns are 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 przedstawiający instrukcje przygotowania śniadania jako osiem zadań asynchronicznych, które kończą się w około 20 minut, gdzie niestety jajka i placki ziemniaczane się spalą.

Aktualizacje kodu poprawiają proces przygotowywania, skracając czas gotowania, ale wprowadzają regresję poprzez przypalanie jajek i placków ziemniaczanych. 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żne

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:

Async Function MakeToastWithButterAndJamAsync(number As Integer) As Task(Of Toast)
    Dim toast As Toast = Await ToastBreadAsync(number)
    ApplyButter(toast)
    ApplyJam(toast)

    Return toast
End Function

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:

Async Function Main() As Task
    Dim cup As Coffee = PourCoffee()
    Console.WriteLine("coffee is ready")

    Dim eggsTask = FryEggsAsync(2)
    Dim hashBrownTask = FryHashBrownsAsync(3)
    Dim toastTask = MakeToastWithButterAndJamAsync(2)

    Dim eggs = Await eggsTask
    Console.WriteLine("eggs are ready")

    Dim hashBrown = Await hashBrownTask
    Console.WriteLine("hash browns are ready")

    Dim toast = Await toastTask
    Console.WriteLine("toast is ready")

    Dim oj As Juice = PourOJ()
    Console.WriteLine("oj is ready")
    Console.WriteLine("Breakfast is ready!")
End Function

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 Async Function ToastBreadAsync(slices As Integer) As Task(Of Toast)
    For slice As Integer = 0 To slices - 1
        Console.WriteLine("Putting a slice of bread in the toaster")
    Next
    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()
End Function

Uwaga / 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 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.vb:line 65
   at AsyncBreakfast.Program.MakeToastWithButterAndJamAsync(Int32 number) in Program.vb:line 36
   at AsyncBreakfast.Program.Main(String[] args) in Program.vb: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 rzucony w właściwości Task.Exception. Zadania obarczone błędem rzucają wyjątek, gdy Await wyrażenie jest stosowane 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 przetworzony i ponownie wyrzucony, gdy kod czeka (Await) na zadanie z błędem.

Gdy kod uruchomiony asynchronicznie zgłasza wyjątek, wyjątek jest przechowywany w obiekcie Task. Właściwość Task.Exception jest obiektem 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 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.

Wskazówka

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, hashBrownTask, toastTask)
Console.WriteLine("Eggs are ready")
Console.WriteLine("Hash browns are ready")
Console.WriteLine("Toast is ready")
Console.WriteLine("Breakfast is ready!")

Inną opcją jest użycie metody WhenAny, która zwraca obiekt Task(Of 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.

Module ConcurrentBreakfastProgram
    Async Function Main() As Task
        Dim cup As Coffee = PourCoffee()
        Console.WriteLine("Coffee is ready")

        Dim eggsTask As Task(Of Egg) = FryEggsAsync(2)
        Dim hashBrownTask As Task(Of HashBrown) = FryHashBrownsAsync(3)
        Dim toastTask As Task(Of Toast) = MakeToastWithButterAndJamAsync(2)

        Dim breakfastTasks As New List(Of Task) From {eggsTask, hashBrownTask, toastTask}
        While breakfastTasks.Count > 0
            Dim finishedTask As Task = Await Task.WhenAny(breakfastTasks)
            If finishedTask Is eggsTask Then
                Console.WriteLine("eggs are ready")
            ElseIf finishedTask Is hashBrownTask Then
                Console.WriteLine("hash browns are ready")
            ElseIf finishedTask Is toastTask Then
                Console.WriteLine("toast is ready")
            End If
            Await finishedTask
            breakfastTasks.Remove(finishedTask)
        End While

        Dim oj As Juice = PourOJ()
        Console.WriteLine("oj is ready")
        Console.WriteLine("Breakfast is ready!")
    End Function

    Async Function MakeToastWithButterAndJamAsync(number As Integer) As Task(Of Toast)
        Dim toast As Toast = Await ToastBreadAsync(number)
        ApplyButter(toast)
        ApplyJam(toast)

        Return toast
    End Function

    Private Async Function ToastBreadAsync(slices As Integer) As Task(Of Toast)
        For slice As Integer = 0 To slices - 1
            Console.WriteLine("Putting a slice of bread in the toaster")
        Next
        Console.WriteLine("Start toasting...")
        Await Task.Delay(3000)
        Console.WriteLine("Remove toast from toaster")

        Return New Toast()
    End Function

    Private Async Function FryHashBrownsAsync(patties As Integer) As Task(Of HashBrown)
        Console.WriteLine($"putting {patties} hash brown patties in the pan")
        Console.WriteLine("cooking first side of hash browns...")
        Await Task.Delay(3000)
        For patty As Integer = 0 To patties - 1
            Console.WriteLine("flipping a hash brown patty")
        Next
        Console.WriteLine("cooking the second side of hash browns...")
        Await Task.Delay(3000)
        Console.WriteLine("Put hash browns on plate")

        Return New HashBrown()
    End Function

    Private Async Function FryEggsAsync(howMany As Integer) As Task(Of Egg)
        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()
    End Function

    Private Function PourCoffee() As Coffee
        Console.WriteLine("Pouring coffee")
        Return New Coffee()
    End Function

    Private Function PourOJ() As Juice
        Console.WriteLine("Pouring orange juice")
        Return New Juice()
    End Function

    Private Sub ApplyJam(toast As Toast)
        Console.WriteLine("Putting jam on the toast")
    End Sub

    Private Sub ApplyButter(toast As Toast)
        Console.WriteLine("Putting butter on the toast")
    End Sub
End Module

W pobliżu końca fragmentu kodu zwróć uwagę na wyrażenie Await finishedTask. Ten wiersz jest ważny, ponieważ Task.WhenAny zwraca wartość — Task(Of Task) zadanie otoki zawierające ukończone zadanie. Await Task.WhenAnyGdy oczekujesz na ukończenie zadania otoki, a wynikiem jest rzeczywiste zadanie, które zostało ukończone jako pierwsze. Jednak aby pobrać wynik tego zadania lub upewnić się, że wszystkie wyjątki są prawidłowo zgłaszane, musisz wykonać Await samo zadanie (przechowywane w pliku finishedTask). Mimo że wiesz, że zadanie zostało ukończone, oczekiwanie na to ponownie umożliwia uzyskanie dostępu do jego wyniku lub obsługę wszelkich wyjątków, które mogły spowodować jego błąd.

Przejrzyj końcowy kod

Oto jak wygląda ostateczna wersja kodu:

Module ConcurrentBreakfastProgram
    Async Function Main() As Task
        Dim cup As Coffee = PourCoffee()
        Console.WriteLine("Coffee is ready")

        Dim eggsTask As Task(Of Egg) = FryEggsAsync(2)
        Dim hashBrownTask As Task(Of HashBrown) = FryHashBrownsAsync(3)
        Dim toastTask As Task(Of Toast) = MakeToastWithButterAndJamAsync(2)

        Dim breakfastTasks As New List(Of Task) From {eggsTask, hashBrownTask, toastTask}
        While breakfastTasks.Count > 0
            Dim finishedTask As Task = Await Task.WhenAny(breakfastTasks)
            If finishedTask Is eggsTask Then
                Console.WriteLine("eggs are ready")
            ElseIf finishedTask Is hashBrownTask Then
                Console.WriteLine("hash browns are ready")
            ElseIf finishedTask Is toastTask Then
                Console.WriteLine("toast is ready")
            End If
            Await finishedTask
            breakfastTasks.Remove(finishedTask)
        End While

        Dim oj As Juice = PourOJ()
        Console.WriteLine("oj is ready")
        Console.WriteLine("Breakfast is ready!")
    End Function

    Async Function MakeToastWithButterAndJamAsync(number As Integer) As Task(Of Toast)
        Dim toast As Toast = Await ToastBreadAsync(number)
        ApplyButter(toast)
        ApplyJam(toast)

        Return toast
    End Function

    Private Async Function ToastBreadAsync(slices As Integer) As Task(Of Toast)
        For slice As Integer = 0 To slices - 1
            Console.WriteLine("Putting a slice of bread in the toaster")
        Next
        Console.WriteLine("Start toasting...")
        Await Task.Delay(3000)
        Console.WriteLine("Remove toast from toaster")

        Return New Toast()
    End Function

    Private Async Function FryHashBrownsAsync(patties As Integer) As Task(Of HashBrown)
        Console.WriteLine($"putting {patties} hash brown patties in the pan")
        Console.WriteLine("cooking first side of hash browns...")
        Await Task.Delay(3000)
        For patty As Integer = 0 To patties - 1
            Console.WriteLine("flipping a hash brown patty")
        Next
        Console.WriteLine("cooking the second side of hash browns...")
        Await Task.Delay(3000)
        Console.WriteLine("Put hash browns on plate")

        Return New HashBrown()
    End Function

    Private Async Function FryEggsAsync(howMany As Integer) As Task(Of Egg)
        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()
    End Function

    Private Function PourCoffee() As Coffee
        Console.WriteLine("Pouring coffee")
        Return New Coffee()
    End Function

    Private Function PourOJ() As Juice
        Console.WriteLine("Pouring orange juice")
        Return New Juice()
    End Function

    Private Sub ApplyJam(toast As Toast)
        Console.WriteLine("Putting jam on the toast")
    End Sub

    Private Sub ApplyButter(toast As Toast)
        Console.WriteLine("Putting butter on the toast")
    End Sub
End Module

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.

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.

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.

Async/await kontra ContinueWith

Słowa kluczowe Async i Await oferują uproszczenie składniowe w porównaniu do bezpośredniego użycia ContinueWith. Chociaż Async/Await i ContinueWith mają podobne semantyki do obsługi operacji asynchronicznych, kompilator nie musi tłumaczyć Await wyrażeń bezpośrednio na ContinueWith wywołania metod. Zamiast tego kompilator generuje zoptymalizowany kod maszyny stanu, który zapewnia takie samo zachowanie logiczne. Ta transformacja zapewnia znaczną poprawę czytelności i łatwość w utrzymaniu, zwłaszcza w przypadku łączenia wielu operacji asynchronicznych.

Rozważmy scenariusz, w którym należy wykonać wiele sekwencyjnych operacji asynchronicznych. Oto jak wygląda ta sama logika po zaimplementowaniu przy użyciu ContinueWith w porównaniu z Async/Await.

Korzystanie z ContinueWith

Z ContinueWith, każdy krok w sekwencji operacji asynchronicznych wymaga zagnieżdżonych kontynuacji.

' Using ContinueWith - demonstrates the complexity when chaining operations
Function MakeBreakfastWithContinueWith() As Task
    Return StartCookingEggsAsync() _
        .ContinueWith(Function(eggsTask)
                          Dim eggs = eggsTask.Result
                          Console.WriteLine("Eggs ready, starting bacon...")
                          Return StartCookingBaconAsync()
                      End Function) _
        .Unwrap() _
        .ContinueWith(Function(baconTask)
                          Dim bacon = baconTask.Result
                          Console.WriteLine("Bacon ready, starting toast...")
                          Return StartToastingBreadAsync()
                      End Function) _
        .Unwrap() _
        .ContinueWith(Function(toastTask)
                          Dim toast = toastTask.Result
                          Console.WriteLine("Toast ready, applying butter...")
                          Return ApplyButterAsync(toast)
                      End Function) _
        .Unwrap() _
        .ContinueWith(Function(butteredToastTask)
                          Dim butteredToast = butteredToastTask.Result
                          Console.WriteLine("Butter applied, applying jam...")
                          Return ApplyJamAsync(butteredToast)
                      End Function) _
        .Unwrap() _
        .ContinueWith(Sub(finalToastTask)
                          Dim finalToast = finalToastTask.Result
                          Console.WriteLine("Breakfast completed with ContinueWith!")
                      End Sub)
End Function

Korzystanie z Async/Await

Ta sama sekwencja operacji przy użyciu Async/Await jest znacznie bardziej naturalna.

' Using Async/Await - much cleaner and easier to read
Async Function MakeBreakfastWithAsyncAwait() As Task
    Dim eggs = Await StartCookingEggsAsync()
    Console.WriteLine("Eggs ready, starting bacon...")
    
    Dim bacon = Await StartCookingBaconAsync()
    Console.WriteLine("Bacon ready, starting toast...")
    
    Dim toast = Await StartToastingBreadAsync()
    Console.WriteLine("Toast ready, applying butter...")
    
    Dim butteredToast = Await ApplyButterAsync(toast)
    Console.WriteLine("Butter applied, applying jam...")
    
    Dim finalToast = Await ApplyJamAsync(butteredToast)
    Console.WriteLine("Breakfast completed with Async/Await!")
End Function

Dlaczego preferowana jest funkcja Async/Await

Podejście Async/Await oferuje kilka zalet:

  • Czytelność: Kod wygląda jak kod synchroniczny, co ułatwia zrozumienie przepływu operacji.
  • Łatwość konserwacji: dodawanie lub usuwanie kroków w sekwencji wymaga minimalnych zmian kodu.
  • Obsługa błędów: Obsługa wyjątków z użyciem bloków Try/Catch działa naturalnie, podczas gdy ContinueWith wymaga starannej obsługi zadań z błędami.
  • Debugowanie: środowisko stosu wywołań i debugera jest znacznie lepsze dzięki funkcji Async/Await.
  • Wydajność: Optymalizacje kompilatora dla Async/Await są bardziej zaawansowane niż łańcuchy manualne.ContinueWith

Korzyść staje się jeszcze bardziej widoczna w miarę wzrostu liczby operacji łańcuchowych. Chociaż pojedyncza kontynuacja może być zarządzana za pomocą ContinueWith, sekwencje 3–4 lub więcej operacji asynchronicznych szybko stają się trudne do odczytania i utrzymania. Ten wzorzec, znany jako "monadic do-notation" w programowaniu funkcjonalnym, umożliwia tworzenie wielu operacji asynchronicznych w sekwencyjny, czytelny sposób.

Zobacz także