Поделиться через


Асинхронное программирование с помощью Async и Await (Visual Basic)

Модель асинхронного программирования задач (TAP) предоставляет уровень абстракции над типичным асинхронным кодированием. В этой модели код записывается как последовательность операторов, так же, как обычно. Разница в том, что вы можете читать код на основе задач одновременно с тем, как компилятор обрабатывает каждую инструкцию, и перед тем как он начнет обработку следующей инструкции. Для выполнения этой модели компилятор выполняет множество преобразований для выполнения каждой задачи. Некоторые инструкции могут инициировать работу и возвращать объект Task, представляющий текущую работу, и компилятор должен устранить эти преобразования. Цель асинхронного программирования задач — сделать код, который воспринимается как последовательность операторов, но исполняется в более сложной последовательности. Исполнение основано на выделении внешних ресурсов и завершении задач.

Модель асинхронного программирования задачи аналогична тому, как люди дают инструкции для процессов, включающих асинхронные задачи. В этой статье приводится пример с инструкциями по приготовлению завтрака, чтобы показать, как ключевые слова Async и Await упрощают понимание кода, содержащего серию асинхронных инструкций. Инструкции по созданию завтрака могут быть предоставлены в виде списка:

  1. Налить чашку кофе.
  2. Разогрейте кастрюлю, а затем жарите два яйца.
  3. Приготовьте три картофельные котлеты.
  4. Поджарьте два кусочка хлеба.
  5. Разложите масло и варенье на тост.
  6. Налить стакан апельсинового сока.

Если у вас есть опыт приготовления пищи, вы можете выполнить эти инструкции асинхронно. Вы начинаете разогревать сковороду для яиц, а затем начинаете готовить картофельные оладьи. Вы положили хлеб в тостер, а затем начните готовить яйца. На каждом шаге процесса вы запускаете задачу, а затем переходите к другим задачам, которые готовы к вашему внимания.

Приготовление завтрака является хорошим примером асинхронной работы, которая не параллельна. Один человек (или поток) может обрабатывать все задачи. Один человек может готовить завтрак без задержек, начиная следующую задачу до завершения предыдущей задачи. Каждая задача приготовления пищи выполняется независимо от того, активно ли кто-то наблюдает за процессом. Как только вы поставите сковороду на огонь для приготовления яиц, можете начать жарить драники. После того как картофельные оладьи начнут готовиться, можно положить хлеб в тостер.

Для параллельного алгоритма требуется несколько людей, которые готовят (или несколько потоков). Один человек готовит яйца, другой готовит картофельные оладьи, и т. д. Каждый человек фокусируется на одной конкретной задаче. Каждый человек, который готовит (или каждый поток), синхронно блокируется, ожидая завершения текущей задачи: драники готовы для переворачивания, хлеб готов подняться в тостере и т. д.

Диаграмма, которая показывает инструкции по приготовлению завтрака в виде списка из семи последовательных задач, выполняемых за 30 минут.

Рассмотрим тот же список синхронных инструкций, написанных как инструкции кода 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

Если вы интерпретируете эти инструкции как компьютер, завтрак занимает около 30 минут, чтобы подготовиться. Длительность — это сумма времени отдельной задачи. Компьютер блокирует каждую инструкцию до завершения всех работ, а затем переходит к следующей инструкции задачи. Этот подход может занять значительное время. В примере завтрака метод компьютера создает ненасытный завтрак. Поздние задачи в синхронном списке, такие как поджаривание хлеба, не начинаются, пока не завершатся предыдущие задачи. Некоторая еда остывает, прежде чем завтрак готов к подаче.

Если вы хотите, чтобы компьютер выполнял инструкции асинхронно, необходимо написать асинхронный код. При написании клиентских программ необходимо, чтобы пользовательский интерфейс реагировал на входные данные пользователей. Приложение не должно заморозить все взаимодействие при скачивании данных из Интернета. При написании серверных программ не требуется блокировать потоки, которые могут обслуживать другие запросы. Использование синхронного кода, когда существуют асинхронные альтернативы, вредит вашей способности более экономно масштабироваться. Вы оплачиваете заблокированные потоки.

Для успешных современных приложений требуется асинхронный код. Без поддержки языка при написании асинхронного кода требуются обратные вызовы, события завершения или другие средства, которые скрывают исходное намерение кода. Преимущество синхронного кода — это пошаговое действие, которое упрощает сканирование и понимание. Традиционные асинхронные модели позволяют сосредоточиться на асинхронном характере кода, а не на фундаментальных действиях кода.

Не блокируйте, ожидайте вместо этого

Предыдущий код подчеркивает неудачную практику программирования: написание синхронного кода для выполнения асинхронных операций. Код блокирует текущий поток от выполнения любой другой работы. Код не прерывает поток во время выполнения задач. Результат этой модели похож на то, как вы смотрите на тостер после того, как засунули в него хлеб. Вы игнорируете любые прерывания и не запускаете другие задачи, пока не появится хлеб. Вы не берете масло и варенье из холодильника. Возможно, вы не увидите пожар, начинающийся на плите. Вы хотите одновременно и поджаривать хлеб, и заниматься другими делами. То же самое верно для твоего кода.

Для начала можно обновить код, чтобы поток не блокируется во время выполнения задач. Ключевое слово Await предоставляет неблокирующий способ начать задачу, а затем продолжить выполнение после её завершения. Простая асинхронная версия кода завтрака выглядит так:

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

Код обновляет тела исходных методов FryEggs, FryHashBrowns и ToastBread, чтобы возвращать объекты Task(Of Egg), Task(Of HashBrown) и Task(Of Toast) соответственно. Обновленные имена методов включают суффикс Async: FryEggsAsync, FryHashBrownsAsyncи ToastBreadAsync. Функция Main возвращает Task объект, хотя у него нет Return выражения, что так задумано.

Замечание

Обновленный код еще не использует ключевые функции асинхронного программирования, что может привести к более короткому времени завершения. Код обрабатывает задачи примерно столько же времени, сколько и начальная синхронная версия. Полные реализации методов см. в окончательной версии кода далее в этой статье.

Давайте применим пример завтрака к обновленному коду. Поток не блокируется во время приготовления яиц или хэш-браунов, но код при этом не запускает другие задачи до тех пор, пока выполнение текущей задачи не завершится. Вы по-прежнему кладете хлеб в тостер и следите за тостером, пока хлеб не выскакивает, но теперь вы можете реагировать на отвлечения. В ресторане, где делают несколько заказов, повар может начать новый заказ, пока другой уже готовится.

В обновленном коде поток, работающий над завтраком, не блокируется во время ожидания любой запущенной задачи, которая не завершена. Для некоторых приложений это изменение необходимо. Вы можете включить ваше приложение для поддержки взаимодействия с пользователем во время загрузки данных из Интернета. В других сценариях может потребоваться запустить другие задачи, ожидая завершения предыдущей задачи.

Одновременные запуски задач

Для большинства операций необходимо немедленно запустить несколько независимых задач. По завершении каждой задачи вы инициируете другую работу, готовую к началу. При применении этой методологии к примеру завтрака вы можете быстрее подготовить завтрак. Вы также готовите все практически в одно и то же время, чтобы вы могли насладиться горячим завтраком.

Класс Task и связанные типы — это классы, которые можно использовать для применения этого стиля рассуждений к задачам, которые выполняются. Этот подход позволяет писать код, который более тесно похож на то, как вы создаете завтрак в реальной жизни. Вы начинаете одновременно готовить яйца, картофельные оладьи и тост. Так как каждый элемент пищи требует действий, вы обращаете внимание на эту задачу, заботитесь о действии, а затем ждете чего-то другого, требующего вашего внимания.

В коде вы запускаете задачу и удерживаете объект Task, представляющий работу. Вы используете метод Await для задачи, чтобы отсрочить выполнение работы до тех пор, пока результат не будет готов.

Примените эти изменения к коду завтрака. Первым шагом является хранение задач для операций при запуске, а не использование выражения 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!")

Эти изменения не ускоряют подготовку вашего завтрака. Выражение Await применяется ко всем задачам сразу после их запуска. Следующим шагом является перемещение Await выражений для картофельных оладий и яиц в конец метода, прежде чем подавать завтрак.

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

Теперь у вас есть асинхронно подготовленный завтрак, который занимает около 20 минут, чтобы подготовиться. Общее время приготовления уменьшается, так как некоторые задачи выполняются одновременно.

Схема с инструкциями по приготовлению завтрака в виде восьми асинхронных задач, которые завершаются примерно за 20 минут, хотя, к сожалению, яйца и драники подгорают.

Обновления кода улучшают процесс подготовки, уменьшая время приготовления, но они вводят регрессию путем сжигания яиц и хэш-браунов. Вы одновременно запускаете все асинхронные задачи. Вы ожидаете выполнения каждой задачи только тогда, когда вам нужны результаты. Код может быть похож на программу в веб-приложении, которая выполняет запросы к различным микрослужбам, а затем объединяет результаты на одну страницу. Вы выполняете все запросы немедленно, а затем применяете выражение Await ко всем этим задачам и создаете веб-страницу.

Поддержка сочетания с задачами

Предыдущие редакции кода помогают подготовить всё к завтраку одновременно, за исключением тоста. Процесс приготовления тостов является композицией асинхронной операции (поджаривание хлеба) с синхронными операциями (намазывание масла и джема на тост). В этом примере показана важная концепция асинхронного программирования:

Это важно

Композиция асинхронной операции, за которой следует синхронная работа, является асинхронной операцией. Другими словами, если любая часть операции является асинхронной, то вся операция является асинхронной.

В предыдущих обновлениях вы узнали, как использовать объекты Task или Task<TResult> для выполнения задач. Вы ожидаете каждой задачи, прежде чем использовать его результат. Следующим шагом является создание методов, представляющих сочетание других работ. Прежде чем подавать завтрак, подождите, пока закончится задача, которая подразумевает поджаривание хлеба, прежде чем вы намазываете хлеб маслом и вареньем.

Вы можете представить эту работу со следующим кодом:

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

Метод MakeToastWithButterAndJamAsync имеет модификатор Async в сигнатуре, который сигнализирует компилятору о том, что метод содержит выражение Await и содержит асинхронные операции. Метод заключается в поджаривании хлеба, а затем намазывании масла и варенья. Метод возвращает объект Task<TResult>, представляющий состав трех операций.

Измененный основной блок кода теперь выглядит следующим образом:

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

Это изменение кода иллюстрирует важный способ работы с асинхронным кодом. Вы создаете задачи, разделяя операции на новый метод, который возвращает задачу. Вы можете выбрать, когда ждать этой задачи. Вы можете одновременно запускать другие задачи.

Обработка асинхронных исключений

До этого момента код неявно предполагает успешное выполнение всех задач. Асинхронные методы вызывают исключения, как и их синхронные аналоги. Цели асинхронной поддержки исключений и обработки ошибок совпадают с асинхронной поддержкой в целом. Наилучшей практикой является писать код, который читается как ряд синхронных операторов. Задачи вызывают исключения, когда они не могут завершиться успешно. Клиентский код может перехватывать эти исключения при применении выражения Await к запущенной задаче.

В примере с завтраком предположим, что тостер загорится во время поджаривания хлеба. Эту проблему можно имитировать, изменив метод ToastBreadAsync, чтобы он соответствовал следующему коду:

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

Замечание

При компиляции этого кода отображается предупреждение о недоступном коде. Эта ошибка сделана намеренно. После того, как тостер загорается, операции не выполняются нормально, и код возвращает ошибку.

После внесения изменений кода запустите приложение и проверьте выходные данные:

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)

Обратите внимание, что довольно много задач завершается между временем, когда тостер загорается и система наблюдает исключение. Когда задача, которая выполняется асинхронно, создает исключение, эта задача неисправна. Объект Task содержит исключение, которое было создано в свойстве Task.Exception . Неисправные задачи выбрасывают исключение, когда Await выражение применяется к задаче.

Существует два важных механизма для понимания этого процесса.

  • Как исключение хранится в неисправной задаче.
  • Как исключение распаковывается и повторно выбрасывается при ожидании кода (Await) на аварийной задаче.

При асинхронном выполнении кода, когда выбрасывается исключение, оно хранится в объекте Task. Свойство Task.Exception является AggregateException объектом, так как во время асинхронной работы может возникать несколько исключений. Всякое выброшенное исключение добавляется в коллекцию AggregateException.InnerExceptions. Если свойство Exception имеет значение NULL, создается новый объект AggregateException, а исключение вызывается первым элементом в коллекции.

Наиболее распространенный сценарий для неисправной задачи заключается в том, что свойство Exception содержит ровно одно исключение. Когда ваш код ожидает задачи с ошибкой, он повторно выбрасывает первое исключение AggregateException.InnerExceptions из коллекции. Результатом этого является причина, по которой выходные данные из примера показывают объект InvalidOperationException, а не объект AggregateException. Извлечение первого внутреннего исключения приближает работу с асинхронными методами к работе с их синхронными аналогами максимально похожей. Вы можете проверить свойство Exception в коде, если сценарий может создать несколько исключений.

Подсказка

Рекомендуется использовать любые исключения проверки аргументов, которые возникают синхронно из методов возврата задач. Для получения дополнительной информации и примеров см. раздел Исключения в методах, возвращающих задачи.

Прежде чем продолжить переход к следующему разделу, закомментируйте следующие две инструкции в методе ToastBreadAsync. Вы не хотите начать еще один огонь:

' Console.WriteLine("Fire! Toast is ruined!")
' Throw New InvalidOperationException("The toaster is on fire")

Эффективное применение выражений await к задачам

Вы можете улучшить ряд выражений Await в конце предыдущего кода с помощью методов класса Task. Один API — это метод WhenAll, который возвращает объект Task, который завершается после завершения всех задач в списке аргументов. Следующий код демонстрирует этот метод:

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

Другой вариант — использовать метод WhenAny, который возвращает объект Task(Of Task), который завершается после завершения любого из его аргументов. Вы можете ждать возвращаемой задачи, так как вы знаете, что задача выполнена. В следующем коде показано, как использовать метод WhenAny, чтобы ждать завершения первой задачи, а затем обработать его результат. После обработки результата из завершенной задачи удалите завершенную задачу из списка задач, переданных методу 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

В конце фрагмента кода обратите внимание на выражение Await finishedTask. Эта строка важна, так как Task.WhenAny возвращается Task(Of Task) задача-оболочка, содержащая завершенную задачу. Когда вы Await Task.WhenAnyожидаете завершения задачи-оболочки, и результатом является фактическая задача, завершающаяся первым. Тем не менее, чтобы получить результат этой задачи или убедиться, что все исключения создаются должным образом, необходимо выполнить Await завершенную задачу (хранящуюся в finishedTask). Даже если вы знаете, что задача завершена, ожидая его снова, вы сможете получить доступ к его результату или обработать все исключения, которые могли вызвать ошибку.

Просмотр окончательного кода

Вот как выглядит окончательная версия кода:

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

диаграмме, которая показывает инструкции по подготовке завтрака в качестве шести асинхронных задач, которые выполняются около 15 минут, и код отслеживает возможные прерывания.

Код завершает асинхронные задачи завтрака за примерно 15 минут. Общее время сокращается, так как некоторые задачи выполняются одновременно. Код одновременно отслеживает несколько задач и выполняет действия только по мере необходимости.

Окончательный код является асинхронным. Это более точно отражает, как человек может готовить завтрак. Сравните окончательный код с первым примером кода в статье. Основные действия по-прежнему понятны, считывая код. Вы можете прочитать окончательный код так же, как вы читаете список инструкций по созданию завтрака, как показано в начале статьи. Функции языка для ключевых слов Async и Await предоставляют перевод, который каждый человек делает, чтобы следовать письменным инструкциям: начинать задачи, когда можно, и не блокировать выполнение, ожидая завершения задач.

Async/await vs ContinueWith

Ключевые слова Async и Await обеспечивают синтаксическое упрощение по сравнению с использованием ContinueWith напрямую. Хотя Async/Await и ContinueWith имеют аналогичную семантику для обработки асинхронных операций, компилятор не обязательно преобразует Await выражения непосредственно в ContinueWith вызовы методов. Вместо этого компилятор создает оптимизированный код компьютера состояния, который обеспечивает то же логическое поведение. Это преобразование обеспечивает значительные преимущества удобочитаемости и удобства обслуживания, особенно при цепочке нескольких асинхронных операций.

Рассмотрим сценарий, в котором необходимо выполнить несколько последовательных асинхронных операций. Вот как выглядит та же логика при реализации с помощью ContinueWith по сравнению с Async/Await:

Использование метода ContinueWith

Для ContinueWithкаждого шага в последовательности асинхронных операций требуются вложенные продолжения:

' 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

Использование Async/Await

Та же последовательность операций с использованием Async/Await выглядит гораздо более естественно.

' 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

Почему предпочтительнее async и await

Подход Async/Await предлагает несколько преимуществ:

  • Удобочитаемость: код считывается как синхронный код, что упрощает понимание потока операций.
  • Удобство обслуживания. Для добавления или удаления шагов в последовательности требуются минимальные изменения кода.
  • Обработка ошибок: обработка исключений с Try/Catch блоками работает естественно, в то время как ContinueWith требует тщательной обработки неисправных задач.
  • Отладка: стек вызовов и интерфейс отладчика гораздо лучше сAsync/Await.
  • Производительность: Оптимизации компилятора для Async/Await более сложны и изощрённы, чем ручные цепочкиContinueWith.

Преимущество становится еще более очевидным по мере увеличения числа операций в цепочке. Хотя одно продолжение может быть управляемо с помощью ContinueWith, последовательности из 3-4 или более асинхронных операций быстро становятся сложными для чтения и обслуживания. Этот шаблон, известный как "monadic do-notation" в функциональном программировании, позволяет составлять несколько асинхронных операций последовательно, читабельным образом.

См. также