1. Основы async/await и обработка ошибок
Основы async/await и обработка ошибок
Асинхронное программирование — фундамент отзывчивых мобильных приложений. До появления Swift 5.5 разработчики iOS полагались на Grand Central Dispatch (GCD) и замыкания (closures). Этот подход часто приводил к глубокой вложенности вызовов и усложнял чтение логики. Современный инструмент Swift Concurrency решает эту проблему, предлагая линейный и безопасный синтаксис.
Ключевые слова async и await позволяют писать асинхронный код так же просто, как и синхронный. Под капотом система сама управляет потоками, освобождая их во время ожидания результата.
Синтаксис и точки приостановки
Чтобы объявить функцию асинхронной, необходимо добавить ключевое слово async перед типом возвращаемого значения. Это сигнализирует компилятору о том, что внутри функции могут находиться точки приостановки (suspension points).
Точка приостановки — это место в коде, где выполнение функции временно останавливается, а текущий поток возвращается системе для выполнения других задач. Когда асинхронная операция завершается, функция возобновляет работу.
Для вызова такой функции используется ключевое слово await. Оно указывает компилятору, что в этом месте код может быть приостановлен. Важно понимать, что await не блокирует поток, а лишь ставит выполнение текущей задачи на паузу.
> Асинхронный код — это двигатель современных приложений. Он даёт возможность загружать данные с сервера, обновлять интерфейс и выполнять сложные вычисления в фоне, оставляя пользователя в полной уверенности, что всё работает плавно. > > Хабр
Сравнение подходов
Чтобы наглядно оценить преимущества нового синтаксиса, сравним классический подход на базе замыканий и современный подход.
| Характеристика | Замыкания (GCD) | Swift Concurrency (async/await) | | --- | --- | --- | | Читаемость | Низкая (эффект "пирамиды смерти") | Высокая (линейный код) | | Обработка ошибок | Ручная проверка Result или Error | Встроенная через do-catch и throws | | Управление памятью | Требует [weak self] для избежания утечек | Автоматическое, риск утечек минимален | | Возврат значений | Через аргументы замыкания | Прямой возврат через return |
Переход на новый синтаксис значительно сокращает объем шаблонного кода.
Экономия строк кода = (Старые строки - Новые строки) / Старые строки × 100. Если функция с замыканиями занимала 25 строк, а переписанная на async/await занимает 10 строк, экономия составит 60%. Это напрямую влияет на скорость ревью кода и поддержку проекта.
Механика работы потоков под капотом
Чтобы по-настоящему понять ценность нового подхода, необходимо разобраться в том, как система управляет ресурсами. В традиционном GCD при блокировке потока (например, с помощью семафоров или синхронных очередей) поток простаивает. Операционной системе приходится создавать новые потоки для обработки других задач, что ведет к перерасходу оперативной памяти и процессорного времени.
При использовании await происходит неблокирующее ожидание. Когда функция достигает точки приостановки, она упаковывает свое текущее состояние (локальные переменные, точку возврата) в специальную структуру в куче (heap). Поток, на котором выполнялась функция, освобождается и возвращается в системный пул.
Количество потоков в пуле Swift Concurrency строго контролируется. Если — количество ядер процессора, а — количество активных потоков пула, то система поддерживает правило . Например, для 6-ядерного процессора () система создаст не более 6 активных потоков () для выполнения асинхронных задач. Это полностью исключает проблему Thread Explosion (взрывного роста потоков), которая часто возникала при злоупотреблении глобальными очередями GCD.
Обработка ошибок в асинхронном контексте
В реальных мобильных приложениях сетевые запросы или операции с файловой системой часто завершаются неудачей. Сервер может вернуть ошибку, соединение может прерваться, а данные могут оказаться поврежденными. Для обработки таких ситуаций используется комбинация async и throws.
Ключевое слово throws ставится после async и означает, что функция может выбросить ошибку. При вызове такой функции необходимо использовать try await.
В этом примере используется встроенный асинхронный метод URLSession.shared.data(from:). Если сервер возвращает статус, отличный от 200, функция выбрасывает кастомную ошибку NetworkError.invalidResponse.
Использование do-catch
Чтобы перехватить и обработать выброшенную ошибку, асинхронный вызов оборачивается в блок do-catch. Это позволяет централизованно управлять логикой восстановления после сбоев или показывать пользователю соответствующие уведомления.
do и поместите в него вызовы с try await.catch для перехвата конкретных типов ошибок.Количество обрабатываемых байт может варьироваться. Если сервер возвращает JSON размером 1500 байт, переменная data.count будет равна 1500. При скорости интернета 5 мегабит в секунду загрузка такого объема произойдет практически мгновенно, но await все равно корректно освободит поток на эти доли секунды.
Интеграция с синхронным кодом через Task
Одной из главных проблем при внедрении Swift Concurrency является вызов асинхронных функций из синхронного контекста. Например, методы жизненного цикла UIViewController (такие как viewDidLoad) являются синхронными. Вы не можете просто написать await внутри них.
Для решения этой задачи используется Task — базовая единица асинхронной работы. Task создает новый асинхронный контекст, позволяя запускать приостанавливаемые функции откуда угодно.
Внутри замыкания Task код выполняется последовательно. Сначала система дождется завершения fetchUserProfile, и только после успешного получения результата вызовет метод display. Если произойдет ошибка, выполнение немедленно перейдет в блок catch.
Важно отметить, что Task по умолчанию наследует приоритет и контекст выполнения (например, главный поток, если он вызван из UIViewController). Это избавляет от необходимости вручную переключаться на DispatchQueue.main для обновления пользовательского интерфейса, что было частой причиной сбоев при использовании GCD.
Отмена задач (Cancellation)
Еще одним важным аспектом основ является кооперативная отмена задач. В отличие от принудительного завершения потоков, Swift Concurrency просит задачу остановиться, но сама задача должна проверить, не была ли она отменена.
Если пользователь закрывает экран до завершения загрузки, мы можем отменить связанный Task. Функция Task.checkCancellation() выбросит ошибку CancellationError, которая прервет выполнение цикла и передаст управление в блок catch. Это позволяет безопасно освобождать ресурсы и избегать выполнения ненужной работы.