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

Одновременно: async let

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

Два await подряд так не умеют: второй вызов начинается, только когда закончился первый. Чтобы две работы шли одновременно, в Swift есть async let:

SWIFT
func brew(_ drink: String, ms: Int) async throws -> String {
    try await Task.sleep(for: .milliseconds(ms))
    return drink
}

async let tea = brew("tea", ms: 100)
async let coffee = brew("coffee", ms: 50)
let (first, second) = try await (tea, coffee)
print(first, second) // tea coffee

Две работы сразу

async let объявляет константу, значение которой начинают вычислять сразу, а читают позже. Разберём программу по строкам:

  1. async let tea = brew("tea", ms: 100) запускает приготовление чая и сразу идёт дальше, не дожидаясь результата.
  2. async let coffee = brew("coffee", ms: 50) запускает кофе. Теперь обе работы идут одновременно, и каждая из них — задача: Swift выполняет её отдельно от остального кода.
  3. try await (tea, coffee) ждёт оба напитка и раскладывает их по константам first и second. try нужен, потому что brew может выбросить ошибку.
  4. print(first, second) печатает tea coffee.

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

Где в этой программе стоят await и try:

  • в строке с async let не нужны ни await, ни try: работа там только запускается, а ошибка, если она случится, придёт туда, где значение читают;
  • try await нужен там, где значение читают: если работа ещё не закончилась, программа подождёт её на этой строке.

Порядок результатов

Кофе готовится 50 мс, вдвое быстрее чая. И всё же программа печатает tea coffee, а не coffee tea. Порядок результатов задаёт код: в (tea, coffee) чай стоит первым, а то, какая работа закончилась раньше, на порядок не влияет.

С пейджерами так же: какой бы ни прозвенел первым, чашки вы ставите на поднос в том порядке, какой выбрали сами.

Здесь картинка с двумя бариста неточна. Бариста в кафе всегда двое, а компьютер сам решает, сколько работ выполнять в одно и то же мгновение. Swift обещает другое: обе работы начаты сразу, и ни одна не ждёт конца другой.

Этот вопрос часто задают на собеседовании: что в таком коде выполняется одновременно? Ответ:

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

Частая ошибка

Две работы запустили через await подряд и ждали, что они пойдут одновременно:

SWIFT
let cupOfTea = try await brew("tea", ms: 100) // ✗
let cupOfCoffee = try await brew("coffee", ms: 50)
print(cupOfTea, cupOfCoffee)

Программа печатает то же tea coffee, но ждёт не меньше 150 мс вместо примерно 100: кофе начинают готовить, только когда чай уже готов. Компилятор ошибки не покажет, ведь программа работает верно, только медленнее. Исправление — async let, как в примере выше.

Главное

  • async let запускает работу сразу и не ждёт её; ждут там, где значение читают через await.
  • Работы, запущенные через async let, идут одновременно, и общее ожидание примерно равно самой долгой из них.
  • Порядок результатов задаёт код, а не то, какая работа закончилась первой.
  • Два await подряд выполняются по очереди, а не одновременно.