Перейти к содержимому
Теория · Асинхронный код · урок 2 из 7

async и await

Покупатель с пейджером может сделать два заказа подряд. Сначала он заказывает чай, ждёт сигнала и забирает чашку, а уже потом заказывает кофе. Второй заказ начинается, когда первый готов. Так же ведут себя два await подряд.

Ожидания идут по очереди

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

Посмотрим на это в виджете. Функция brew готовит напиток: печатает, что начала, ждёт паузу и печатает, что напиток готов. Длину паузы, 50 мс, хранит константа pause типа Duration. Нажимайте «Следующий шаг» и следите за столбиком вызовов: на каждом await коробка main ждёт, а brew после паузы продолжает со следующей строки.

ВживуюДва await по очередишаг 1 из 8 · рамок: 1

До первого await код идёт как обычно и печатает order.

Следите, где ждёт main и с какой строки продолжает brew

Главное в виджете: вызов brew("coffee") начался, только когда закончился brew("tea"). Поэтому паузы сложились: два заказа по 50 мс заняли не меньше 100 мс.

Этот вопрос часто задают на собеседовании: что выполняется одновременно в нескольких await подряд? Ответ — ничего. Каждый вызов ждёт, пока закончится предыдущий.

Ошибки и try await

Асинхронная функция выбрасывает ошибку через throw, так же как обычная. Ошибка проходит через await к тому, кто ждёт результата, и её ловят в do/catch, как в теме об ошибках:

SWIFT
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
}

Разберём, что происходит:

  1. brewLatte(milk:) объявлена как async throws: она ждёт и может выбросить ошибку.
  2. Вызов brewLatte(milk: 0) сначала ждёт 100 мс на Task.sleep.
  3. Затем guard видит, что молока нет, и функция выбрасывает CafeError.outOfMilk.
  4. Ошибка возвращается к строке с try await, print внутри do не выполняется, и управление переходит в catch.
  5. catch печатает no latte: outOfMilk.

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

Как ждали раньше

До появления async/await ожидание строили на замыканиях. Функция получала обработчик завершения — замыкание, которое она вызовет, когда результат будет готов. Сама функция возвращалась сразу, ещё без результата. Чтобы дождаться двух результатов по очереди, второй вызов писали внутри обработчика первого, и замыкания вкладывались одно в другое.

Чем async/await удобнее обработчика завершения — частый вопрос на собеседовании. Ответ:

  • код читается сверху вниз, а не уходит всё глубже во вложенные замыкания;
  • результат приходит через return, а не параметром замыкания;
  • ошибку выбрасывают через throw и ловят в catch, как в обычном коде;
  • ожидание при этом, как и раньше, не блокирует программу.

Главное

  • Два await подряд выполняются по очереди: следующий вызов начинается, когда закончился предыдущий.
  • На await функция приостанавливается, а потом продолжает со следующей строки, со всеми своими переменными.
  • В отличие от обработчика завершения, асинхронная функция возвращает результат через return и выбрасывает ошибку через throw; ловят ошибку в catch вокруг try await.