$ sudo teach IT

Представьте, что вы заказываете кофе в кофейне. Вы не стоите перед стойкой, замерев, пока бариста готовит напиток — вы отходите, находите свободный столик, проверяете телефон, а когда кофе готов, вас зовут забрать заказ. Вы не тратите время впустую, но и не делаете две вещи буквально одновременно: пока готовится кофе, вы просто занимаетесь чем-то другим, а потом возвращаетесь к прерванному делу.

Программы часто оказываются в похожей ситуации: нужно дождаться ответа от сервера, прочитать файл с диска или просто немного подождать. Если код на это время замирает, всё приложение перестаёт реагировать — окно не перерисовывается, кнопки не нажимаются. Swift решает эту задачу с помощью async и await: способа написать «подождите здесь результат», не замораживая всё остальное.

Асинхронная функция

Функция, которая умеет приостанавливаться в ожидании результата, помечается ключевым словом async после списка параметров, перед стрелкой возврата:

func loadGreeting() async -> String {
    try? await Task.sleep(for: .milliseconds(10))
    return "Привет!"
}

Здесь Task.sleep(for:) — способ имитировать ожидание: подождать немного, как будто идёт настоящий сетевой запрос. Обратите внимание: сама функция ничего не знает про потоки и параллельность — она просто помечена как способная «встать на паузу».

await — точка приостановки

Вызвать асинхронную функцию просто так нельзя — компилятор потребует поставить перед вызовом слово await:

let text = await loadGreeting()
print(text)

await — это не отдельная команда «подожди n секунд», а пометка на месте вызова: «здесь функция может приостановиться, если результат ещё не готов, и вернуть управление, когда он появится». Такое место называется точкой приостановки (suspension point). Пока функция ждёт, поток не заморожен впустую — среда выполнения может в это время заняться чем-то другим, а когда результат придёт, выполнение возобновится ровно с этого места, как будто вы просто сделали паузу в чтении книги на середине предложения.

Важно не путать это с обычным блокирующим ожиданием. await не создаёт новый поток сам по себе и не делает функцию «параллельной» — он лишь описывает возможную паузу. О том, как явно запускать несколько задач по-настоящему одновременно, поговорим отдельно.

Асинхронность внутри асинхронности

Асинхронная функция может вызывать другие асинхронные функции — и это самый частый случай на практике:

func loadUserId() async -> Int {
    try? await Task.sleep(for: .milliseconds(10))
    return 7
}

func loadUserName(_ id: Int) async -> String {
    try? await Task.sleep(for: .milliseconds(10))
    return "Пользователь №\(id)"
}

func loadProfile() async -> String {
    let id = await loadUserId()
    let name = await loadUserName(id)
    return name
}

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

Чем это лучше колбэков

До появления async/await асинхронный код в Swift писали через замыкания-обработчики завершения (completion handler): функция принимала замыкание и вызывала его, когда результат был готов. Каждый следующий шаг превращался в замыкание внутри замыкания, а обработку ошибок приходилось дублировать в каждом таком вложении. Тот же loadProfile в старом стиле выглядел бы примерно так: вызов loadUserId с замыканием, внутри которого — вызов loadUserName с ещё одним замыканием, и так далее вглубь. Чем больше шагов, тем труднее уследить за порядком выполнения и обработкой ошибок.

async/await убирает эту вложенность: асинхронный код выглядит и читается как обычный последовательный, хотя выполняется с паузами. Компилятор сам проверяет, что вы не забыли await там, где он нужен.

Откуда вызывать асинхронную функцию

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

Частые ошибки

Несколько ловушек, в которые попадают почти все новички при первой встрече с асинхронным кодом.

  • Забыть await перед вызовом асинхронной функции. Компилятор сразу покажет ошибку о том, что выражение помечено как async, но вызвано без await — это не пропустят на этапе сборки.
  • Думать, что async сам по себе делает код многопоточным или более быстрым. async описывает возможность приостановки, а не параллельное выполнение — о параллельном запуске нескольких задач речь пойдёт позже.
  • Пытаться вызвать асинхронную функцию из обычной, не помеченной async, функции напрямую. Компилятор такой вызов не пропустит — обычная функция не является асинхронным контекстом.
  • Путать await с ручной задержкой вроде sleep(). Задержка просто тормозит поток; await лишь отмечает место возможной паузы и возобновления.

Резюме

  • async после параметров функции помечает её как способную приостанавливаться в ожидании результата.
  • await ставится перед вызовом такой функции и отмечает точку приостановки — здесь выполнение может встать на паузу и продолжиться позже.
  • Асинхронный код читается сверху вниз, как обычный последовательный, в отличие от вложенных колбэков.
  • Вызвать асинхронную функцию можно только из асинхронного контекста: другой async-функции или задачи.
  • async/await сами по себе не означают параллельное выполнение — это тема следующих уроков.

Проверьте себя

4 вопроса

Сумма двух загруженных чисел

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

Напишите асинхронную функцию loadSum(_ a: Int, _ b: Int) async -> Int, которая по очереди «загружает» оба числа через loadNumber(_:) (не забудьте await на каждом вызове) и возвращает их сумму.