Представьте, что вы заказываете кофе в кофейне. Вы не стоите перед стойкой, замерев, пока бариста готовит напиток — вы отходите, находите свободный столик, проверяете телефон, а когда кофе готов, вас зовут забрать заказ. Вы не тратите время впустую, но и не делаете две вещи буквально одновременно: пока готовится кофе, вы просто занимаетесь чем-то другим, а потом возвращаетесь к прерванному делу.
Программы часто оказываются в похожей ситуации: нужно дождаться ответа от сервера,
прочитать файл с диска или просто немного подождать. Если код на это время замирает,
всё приложение перестаёт реагировать — окно не перерисовывается, кнопки не нажимаются.
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 на каждом вызове) и возвращает их сумму.