async и await
Покупатель с пейджером может сделать два заказа подряд. Сначала он заказывает чай, ждёт сигнала и забирает чашку, а уже потом заказывает кофе. Второй заказ начинается, когда первый готов. Так же ведут себя два await подряд.
Ожидания идут по очереди
await ждёт, пока вызов закончится, и только потом код переходит к следующей строке. Поэтому два вызова через await подряд выполняются по очереди: второй начинается, когда первый вернул результат.
Посмотрим на это в виджете. Функция brew готовит напиток: печатает, что начала, ждёт паузу и печатает, что напиток готов. Длину паузы, 50 мс, хранит константа pause типа Duration. Нажимайте «Следующий шаг» и следите за столбиком вызовов: на каждом await коробка main ждёт, а brew после паузы продолжает со следующей строки.
1let pause: Duration = .milliseconds(50)2 3func brew(_ drink: String) async throws {4 print("brewing \(drink)")5 try await Task.sleep(for: pause)6 print("\(drink) ready")7}8 9print("order")10try await brew("tea")11try await brew("coffee")12print("done")
До первого await код идёт как обычно и печатает order.
Главное в виджете: вызов brew("coffee") начался, только когда закончился brew("tea"). Поэтому паузы сложились: два заказа по 50 мс заняли не меньше 100 мс.
Этот вопрос часто задают на собеседовании: что выполняется одновременно в нескольких await подряд? Ответ — ничего. Каждый вызов ждёт, пока закончится предыдущий.
Ошибки и try await
Асинхронная функция выбрасывает ошибку через throw, так же как обычная. Ошибка проходит через await к тому, кто ждёт результата, и её ловят в do/catch, как в теме об ошибках:
enum CafeError: Error {
case outOfMilk
}
func brewLatte(milk: Int) async throws -> String {
try await Task.sleep(for: .milliseconds(100))
guard milk > 0 else { throw CafeError.outOfMilk }
return "latte"
}
do {
print(try await brewLatte(milk: 0))
} catch {
print("no latte:", error) // no latte: outOfMilk
}Разберём, что происходит:
brewLatte(milk:)объявлена какasync throws: она ждёт и может выбросить ошибку.- Вызов
brewLatte(milk: 0)сначала ждёт 100 мс наTask.sleep. - Затем
guardвидит, что молока нет, и функция выбрасываетCafeError.outOfMilk. - Ошибка возвращается к строке с
try await,printвнутриdoне выполняется, и управление переходит вcatch. catchпечатаетno latte: outOfMilk.
Результат и ошибка возвращаются привычными return и throw, хотя между вызовом и ответом прошла пауза.
Как ждали раньше
До появления async/await ожидание строили на замыканиях. Функция получала обработчик завершения — замыкание, которое она вызовет, когда результат будет готов. Сама функция возвращалась сразу, ещё без результата. Чтобы дождаться двух результатов по очереди, второй вызов писали внутри обработчика первого, и замыкания вкладывались одно в другое.
Чем async/await удобнее обработчика завершения — частый вопрос на собеседовании. Ответ:
- код читается сверху вниз, а не уходит всё глубже во вложенные замыкания;
- результат приходит через
return, а не параметром замыкания; - ошибку выбрасывают через
throwи ловят вcatch, как в обычном коде; - ожидание при этом, как и раньше, не блокирует программу.
Главное
- Два
awaitподряд выполняются по очереди: следующий вызов начинается, когда закончился предыдущий. - На
awaitфункция приостанавливается, а потом продолжает со следующей строки, со всеми своими переменными. - В отличие от обработчика завершения, асинхронная функция возвращает результат через
returnи выбрасывает ошибку черезthrow; ловят ошибку вcatchвокругtry await.