Примечание
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Модель асинхронного программирования задач (TAP) предоставляет уровень абстракции над типичным асинхронным кодированием. В этой модели код записывается как последовательность операторов, так же, как обычно. Разница в том, что вы можете читать код на основе задач одновременно с тем, как компилятор обрабатывает каждую инструкцию, и перед тем как он начнет обработку следующей инструкции. Для выполнения этой модели компилятор выполняет множество преобразований для выполнения каждой задачи. Некоторые инструкции могут инициировать работу и возвращать объект Task, представляющий текущую работу, и компилятор должен устранить эти преобразования. Цель асинхронного программирования задач — сделать код, который воспринимается как последовательность операторов, но исполняется в более сложной последовательности. Исполнение основано на выделении внешних ресурсов и завершении задач.
Модель асинхронного программирования задачи аналогична тому, как люди дают инструкции для процессов, включающих асинхронные задачи. В этой статье приводится пример с инструкциями по приготовлению завтрака, чтобы показать, как ключевые слова async
и await
упрощают понимание кода, содержащего серию асинхронных инструкций. Инструкции по созданию завтрака могут быть предоставлены в виде списка:
- Налить чашку кофе.
- Разогрейте кастрюлю, а затем жарите два яйца.
- Жарить три среза бекона.
- Поджарьте два кусочка хлеба.
- Разложите масло и варенье на тост.
- Налить стакан апельсинового сока.
Если у вас есть опыт приготовления пищи, вы можете выполнить эти инструкции асинхронно. Вы начинаете разогревать кастрюлю для яиц, а затем начать жарить бекон. Вы положили хлеб в тостер, а затем начните готовить яйца. На каждом шаге процесса вы запускаете задачу, а затем переходите к другим задачам, которые готовы к вашему внимания.
Приготовление завтрака является хорошим примером асинхронной работы, которая не параллельна. Один человек (или поток) может обрабатывать все задачи. Один человек может готовить завтрак без задержек, начиная следующую задачу до завершения предыдущей задачи. Каждая задача приготовления пищи выполняется независимо от того, активно ли кто-то наблюдает за процессом. Как только вы начнете разогревать кастрюлю для яиц, вы можете начать жарить бекон. Как только бекон начнет готовиться, можно положить хлеб в тостер.
Для параллельного алгоритма требуется несколько людей, которые готовят (или несколько потоков). Один человек готовит яйца, другой жарит бекон, и т. д. Каждый человек фокусируется на одной конкретной задаче. Каждый, кто готовит (или каждый поток), блокируется синхронно, ожидая, пока завершится текущая задача: бекон пора переворачивать, хлеб вот-вот всплывет из тостера и т. д.
Рассмотрим тот же список синхронных инструкций, написанных в виде операторов кода C#.
using System;
using System.Threading.Tasks;
namespace AsyncBreakfast
{
// These classes are intentionally empty for the purpose of this example. They are simply marker classes for the purpose of demonstration, contain no properties, and serve no other purpose.
internal class Bacon { }
internal class Coffee { }
internal class Egg { }
internal class Juice { }
internal class Toast { }
class Program
{
static void Main(string[] args)
{
Coffee cup = PourCoffee();
Console.WriteLine("coffee is ready");
Egg eggs = FryEggs(2);
Console.WriteLine("eggs are ready");
Bacon bacon = FryBacon(3);
Console.WriteLine("bacon is ready");
Toast toast = ToastBread(2);
ApplyButter(toast);
ApplyJam(toast);
Console.WriteLine("toast is ready");
Juice oj = PourOJ();
Console.WriteLine("oj is ready");
Console.WriteLine("Breakfast is ready!");
}
private static Juice PourOJ()
{
Console.WriteLine("Pouring orange juice");
return new Juice();
}
private static void ApplyJam(Toast toast) =>
Console.WriteLine("Putting jam on the toast");
private static void ApplyButter(Toast toast) =>
Console.WriteLine("Putting butter on the toast");
private static Toast ToastBread(int slices)
{
for (int slice = 0; slice < slices; slice++)
{
Console.WriteLine("Putting a slice of bread in the toaster");
}
Console.WriteLine("Start toasting...");
Task.Delay(3000).Wait();
Console.WriteLine("Remove toast from toaster");
return new Toast();
}
private static Bacon FryBacon(int slices)
{
Console.WriteLine($"putting {slices} slices of bacon in the pan");
Console.WriteLine("cooking first side of bacon...");
Task.Delay(3000).Wait();
for (int slice = 0; slice < slices; slice++)
{
Console.WriteLine("flipping a slice of bacon");
}
Console.WriteLine("cooking the second side of bacon...");
Task.Delay(3000).Wait();
Console.WriteLine("Put bacon on plate");
return new Bacon();
}
private static Egg FryEggs(int howMany)
{
Console.WriteLine("Warming the egg pan...");
Task.Delay(3000).Wait();
Console.WriteLine($"cracking {howMany} eggs");
Console.WriteLine("cooking the eggs ...");
Task.Delay(3000).Wait();
Console.WriteLine("Put eggs on plate");
return new Egg();
}
private static Coffee PourCoffee()
{
Console.WriteLine("Pouring coffee");
return new Coffee();
}
}
}
Если вы интерпретируете эти инструкции как компьютер, завтрак занимает около 30 минут, чтобы подготовиться. Длительность — это сумма времени отдельной задачи. Компьютер блокирует каждую инструкцию до завершения всех работ, а затем переходит к следующей инструкции задачи. Этот подход может занять значительное время. В примере завтрака метод компьютера создает ненасытный завтрак. Поздние задачи в синхронном списке, такие как поджаривание хлеба, не начинаются, пока не завершатся предыдущие задачи. Некоторая еда остывает, прежде чем завтрак готов к подаче.
Если вы хотите, чтобы компьютер выполнял инструкции асинхронно, необходимо написать асинхронный код. При написании клиентских программ необходимо, чтобы пользовательский интерфейс реагировал на входные данные пользователей. Приложение не должно заморозить все взаимодействие при скачивании данных из Интернета. При написании серверных программ не требуется блокировать потоки, которые могут обслуживать другие запросы. Использование синхронного кода, когда существуют асинхронные альтернативы, вредит вашей способности более экономно масштабироваться. Вы оплачиваете заблокированные потоки.
Для успешных современных приложений требуется асинхронный код. Без поддержки языка при написании асинхронного кода требуются обратные вызовы, события завершения или другие средства, которые скрывают исходное намерение кода. Преимущество синхронного кода — это пошаговое действие, которое упрощает сканирование и понимание. Традиционные асинхронные модели позволяют сосредоточиться на асинхронном характере кода, а не на фундаментальных действиях кода.
Не блокируйте, ожидайте вместо этого
Предыдущий код подчеркивает неудачную практику программирования: написание синхронного кода для выполнения асинхронных операций. Код блокирует текущий поток от выполнения любой другой работы. Код не прерывает поток во время выполнения задач. Результат этой модели похож на то, как вы смотрите на тостер после того, как засунули в него хлеб. Вы игнорируете любые прерывания и не запускаете другие задачи, пока не появится хлеб. Вы не берете масло и варенье из холодильника. Возможно, вы не увидите пожар, начинающийся на плите. Вы хотите одновременно и поджаривать хлеб, и заниматься другими делами. То же самое верно для твоего кода.
Для начала можно обновить код, чтобы поток не блокируется во время выполнения задач. Ключевое слово await
предоставляет неблокирующий способ начать задачу, а затем продолжить выполнение после её завершения. Простая асинхронная версия кода завтрака выглядит так:
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!");
}
Код обновляет тела методов FryEggsAsync
, FryBaconAsync
и ToastBreadAsync
для возврата Task<Egg>
, Task<Bacon>
и Task<Toast>
объектов соответственно. Имена методов включают суффикс Async. Метод Main
возвращает объект Task
, хотя у него нет выражения return
, что предусмотрено по замыслу. Для получения дополнительной информации см. раздел Оценка асинхронной функции, не возвращающей значение.
Заметка
Обновленный код еще не использует ключевые функции асинхронного программирования, что может привести к более короткому времени завершения. Код обрабатывает задачи примерно столько же времени, сколько и начальная синхронная версия. Полные реализации методов см. в окончательной версии кода , представленной далее в этой статье.
Давайте применим пример завтрака к обновленному коду. Поток не блокируется во время приготовления яиц или бекона, однако код также не запускает другие задачи до тех пор, пока текущая работа не завершится. Вы по-прежнему кладете хлеб в тостер и следите за тостером, пока хлеб не выскакивает, но теперь вы можете реагировать на отвлечения. В ресторане, где делают несколько заказов, повар может начать новый заказ, пока другой уже готовится.
В обновленном коде поток, работающий над завтраком, не блокируется во время ожидания любой запущенной задачи, которая не завершена. Для некоторых приложений это изменение необходимо. Вы можете включить ваше приложение для поддержки взаимодействия с пользователем во время загрузки данных из Интернета. В других сценариях может потребоваться запустить другие задачи, ожидая завершения предыдущей задачи.
Одновременные запуски задач
Для большинства операций необходимо немедленно запустить несколько независимых задач. По завершении каждой задачи вы инициируете другую работу, готовую к началу. При применении этой методологии к примеру завтрака вы можете быстрее подготовить завтрак. Вы также готовите все практически в одно и то же время, чтобы вы могли насладиться горячим завтраком.
Класс System.Threading.Tasks.Task и связанные типы — это классы, которые можно использовать для применения этого стиля рассуждений к задачам, которые выполняются. Этот подход позволяет писать код, который более тесно похож на то, как вы создаете завтрак в реальной жизни. Вы начинаете готовить яйца, бекон и тост одновременно. Так как каждый элемент пищи требует действий, вы обращаете внимание на эту задачу, заботитесь о действии, а затем ждете чего-то другого, требующего вашего внимания.
В коде вы запускаете задачу и удерживаете объект Task, представляющий работу. Вы используете метод await
для задачи, чтобы отсрочить выполнение работы до тех пор, пока результат не будет готов.
Примените эти изменения к коду завтрака. Первым шагом является хранение задач для операций при запуске, а не использование выражения await
:
Coffee cup = PourCoffee();
Console.WriteLine("Coffee is ready");
Task<Egg> eggsTask = FryEggsAsync(2);
Egg eggs = await eggsTask;
Console.WriteLine("Eggs are ready");
Task<Bacon> baconTask = FryBaconAsync(3);
Bacon bacon = await baconTask;
Console.WriteLine("Bacon is ready");
Task<Toast> toastTask = ToastBreadAsync(2);
Toast toast = await toastTask;
ApplyButter(toast);
ApplyJam(toast);
Console.WriteLine("Toast is ready");
Juice oj = PourOJ();
Console.WriteLine("Oj is ready");
Console.WriteLine("Breakfast is ready!");
Эти изменения не ускоряют подготовку вашего завтрака. Выражение await
применяется ко всем задачам сразу после их запуска. Следующий шаг — переместить выражения await
для бекона и яиц в конец метода перед тем, как подать завтрак.
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!");
Теперь у вас есть асинхронно подготовленный завтрак, который занимает около 20 минут, чтобы подготовиться. Общее время приготовления уменьшается, так как некоторые задачи выполняются одновременно.
Обновления кода улучшают процесс подготовки, уменьшая время приготовления, но они вводят регрессию путем сжигания яиц и бекона. Вы одновременно запускаете все асинхронные задачи. Вы ожидаете выполнения каждой задачи только тогда, когда вам нужны результаты. Код может быть похож на программу в веб-приложении, которая выполняет запросы к различным микрослужбам, а затем объединяет результаты на одну страницу. Вы выполняете все запросы немедленно, а затем применяете выражение await
ко всем этим задачам и создаете веб-страницу.
Поддержка сочетания с задачами
Предыдущие редакции кода помогают подготовить всё к завтраку одновременно, за исключением тоста. Процесс приготовления тостов является композицией асинхронной операции (поджаривание хлеба) с синхронными операциями (намазывание масла и джема на тост). В этом примере показана важная концепция асинхронного программирования:
Важный
Композиция асинхронной операции, за которой следует синхронная работа, является асинхронной операцией. Другими словами, если любая часть операции является асинхронной, то вся операция является асинхронной.
В предыдущих обновлениях вы узнали, как использовать объекты Task или Task<TResult> для выполнения задач. Вы ожидаете каждой задачи, прежде чем использовать его результат. Следующим шагом является создание методов, представляющих сочетание других работ. Прежде чем подавать завтрак, подождите, пока закончится задача, которая подразумевает поджаривание хлеба, прежде чем вы намазываете хлеб маслом и вареньем.
Вы можете представить эту работу со следующим кодом:
static async Task<Toast> MakeToastWithButterAndJamAsync(int number)
{
var toast = await ToastBreadAsync(number);
ApplyButter(toast);
ApplyJam(toast);
return toast;
}
Метод MakeToastWithButterAndJamAsync
имеет модификатор async
в сигнатуре, который сигнализирует компилятору о том, что метод содержит выражение await
и содержит асинхронные операции. Метод заключается в поджаривании хлеба, а затем намазывании масла и варенья. Метод возвращает объект Task<TResult>, представляющий состав трех операций.
Измененный основной блок кода теперь выглядит следующим образом:
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!");
}
Это изменение кода иллюстрирует важный способ работы с асинхронным кодом. Вы создаете задачи, разделяя операции на новый метод, который возвращает задачу. Вы можете выбрать, когда ждать этой задачи. Вы можете одновременно запускать другие задачи.
Обработка асинхронных исключений
До этого момента код неявно предполагает успешное выполнение всех задач. Асинхронные методы вызывают исключения, как и их синхронные аналоги. Цели асинхронной поддержки исключений и обработки ошибок совпадают с асинхронной поддержкой в целом. Наилучшей практикой является писать код, который читается как ряд синхронных операторов. Задачи вызывают исключения, когда они не могут завершиться успешно. Клиентский код может перехватывать эти исключения при применении выражения await
к запущенной задаче.
В примере с завтраком предположим, что тостер загорится во время поджаривания хлеба. Эту проблему можно имитировать, изменив метод ToastBreadAsync
, чтобы он соответствовал следующему коду:
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();
}
Заметка
При компиляции этого кода отображается предупреждение о недоступном коде. Эта ошибка сделана намеренно. После того, как тостер загорается, операции не выполняются нормально, и код возвращает ошибку.
После внесения изменений кода запустите приложение и проверьте выходные данные:
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)
Обратите внимание, что довольно много задач завершается между временем, когда тостер загорается и система наблюдает исключение. Когда задача, которая выполняется асинхронно, создает исключение, эта задача неисправна. Объект Task
содержит исключение, возникающее в свойстве Task.Exception. Неисправные задачи вызывают исключение, если выражение await
применяется к задаче.
Существует два важных механизма для понимания этого процесса.
- Как исключение хранится в неисправной задаче
- Как исключение распаковывается и повторно выполняется при ожидании кода (
await
) в неисправной задаче
При асинхронном выполнении кода, когда выбрасывается исключение, оно хранится в объекте Task
. Свойство Task.Exception является объектом System.AggregateException, так как во время асинхронной работы может возникать несколько исключений. Всякое выброшенное исключение добавляется в коллекцию AggregateException.InnerExceptions. Если свойство Exception
имеет значение NULL, создается новый объект AggregateException
, а исключение вызывается первым элементом в коллекции.
Наиболее распространенный сценарий для неисправной задачи заключается в том, что свойство Exception
содержит ровно одно исключение. Когда ваш код ожидает задачи с ошибкой, он повторно выбрасывает первое исключение AggregateException.InnerExceptions из коллекции. Результатом этого является причина, по которой выходные данные из примера показывают объект System.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, baconTask, toastTask);
Console.WriteLine("Eggs are ready");
Console.WriteLine("Bacon is ready");
Console.WriteLine("Toast is ready");
Console.WriteLine("Breakfast is ready!");
Другой вариант — использовать метод WhenAny, который возвращает объект Task<Task>
, который завершается после завершения любого из его аргументов. Вы можете ждать возвращаемой задачи, так как вы знаете, что задача выполнена. В следующем коде показано, как использовать метод WhenAny, чтобы ждать завершения первой задачи, а затем обработать его результат. После обработки результата из завершенной задачи удалите завершенную задачу из списка задач, переданных методу WhenAny
.
var breakfastTasks = new List<Task> { eggsTask, baconTask, toastTask };
while (breakfastTasks.Count > 0)
{
Task finishedTask = await Task.WhenAny(breakfastTasks);
if (finishedTask == eggsTask)
{
Console.WriteLine("Eggs are ready");
}
else if (finishedTask == baconTask)
{
Console.WriteLine("Bacon is ready");
}
else if (finishedTask == toastTask)
{
Console.WriteLine("Toast is ready");
}
await finishedTask;
breakfastTasks.Remove(finishedTask);
}
В конце фрагмента кода обратите внимание на выражение await finishedTask;
. Выражение await Task.WhenAny
не ожидает завершения задачи, а ожидает объекта Task
, возвращаемого методом Task.WhenAny
. Результатом метода Task.WhenAny
является завершенная (или неисправная) задача. Лучше подождать с выполнением задачи снова, даже если вы знаете, что задача завершена. Таким образом, вы можете получить результат задачи или обеспечить выброс любого исключения, которое вызывает сбой задачи.
Просмотр окончательного кода
Вот как выглядит окончательная версия кода:
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace AsyncBreakfast
{
// These classes are intentionally empty for the purpose of this example. They are simply marker classes for the purpose of demonstration, contain no properties, and serve no other purpose.
internal class Bacon { }
internal class Coffee { }
internal class Egg { }
internal class Juice { }
internal class Toast { }
class Program
{
static async Task Main(string[] args)
{
Coffee cup = PourCoffee();
Console.WriteLine("coffee is ready");
var eggsTask = FryEggsAsync(2);
var baconTask = FryBaconAsync(3);
var toastTask = MakeToastWithButterAndJamAsync(2);
var breakfastTasks = new List<Task> { eggsTask, baconTask, toastTask };
while (breakfastTasks.Count > 0)
{
Task finishedTask = await Task.WhenAny(breakfastTasks);
if (finishedTask == eggsTask)
{
Console.WriteLine("eggs are ready");
}
else if (finishedTask == baconTask)
{
Console.WriteLine("bacon is ready");
}
else if (finishedTask == toastTask)
{
Console.WriteLine("toast is ready");
}
await finishedTask;
breakfastTasks.Remove(finishedTask);
}
Juice oj = PourOJ();
Console.WriteLine("oj is ready");
Console.WriteLine("Breakfast is ready!");
}
static async Task<Toast> MakeToastWithButterAndJamAsync(int number)
{
var toast = await ToastBreadAsync(number);
ApplyButter(toast);
ApplyJam(toast);
return toast;
}
private static Juice PourOJ()
{
Console.WriteLine("Pouring orange juice");
return new Juice();
}
private static void ApplyJam(Toast toast) =>
Console.WriteLine("Putting jam on the toast");
private static void ApplyButter(Toast toast) =>
Console.WriteLine("Putting butter on the toast");
private static async Task<Toast> ToastBreadAsync(int slices)
{
for (int slice = 0; slice < slices; slice++)
{
Console.WriteLine("Putting a slice of bread in the toaster");
}
Console.WriteLine("Start toasting...");
await Task.Delay(3000);
Console.WriteLine("Remove toast from toaster");
return new Toast();
}
private static async Task<Bacon> FryBaconAsync(int slices)
{
Console.WriteLine($"putting {slices} slices of bacon in the pan");
Console.WriteLine("cooking first side of bacon...");
await Task.Delay(3000);
for (int slice = 0; slice < slices; slice++)
{
Console.WriteLine("flipping a slice of bacon");
}
Console.WriteLine("cooking the second side of bacon...");
await Task.Delay(3000);
Console.WriteLine("Put bacon on plate");
return new Bacon();
}
private static async Task<Egg> FryEggsAsync(int howMany)
{
Console.WriteLine("Warming the egg pan...");
await Task.Delay(3000);
Console.WriteLine($"cracking {howMany} eggs");
Console.WriteLine("cooking the eggs ...");
await Task.Delay(3000);
Console.WriteLine("Put eggs on plate");
return new Egg();
}
private static Coffee PourCoffee()
{
Console.WriteLine("Pouring coffee");
return new Coffee();
}
}
}
Код завершает асинхронные задачи завтрака за примерно 15 минут. Общее время сокращается, так как некоторые задачи выполняются одновременно. Код одновременно отслеживает несколько задач и выполняет действия только по мере необходимости.
Окончательный код является асинхронным. Это более точно отражает, как человек может готовить завтрак. Сравните окончательный код с первым примером кода в статье. Основные действия по-прежнему понятны, считывая код. Вы можете прочитать окончательный код так же, как вы читаете список инструкций по созданию завтрака, как показано в начале статьи. Функции языка для ключевых слов async
и await
предоставляют перевод, который каждый человек делает, чтобы следовать письменным инструкциям: начинать задачи, когда можно, и не блокировать выполнение, ожидая завершения задач.