Почему async/await лучше completion handler?
Сегодня хотелось бы поговорить о преимуществах async/await, в сравнении с привычными нами способами работы с асинхронным кодом. async/await был представлен Apple на сессии WWDC 21, но к сожалению, не во всех проектах решаются перейти на данную технологию. Давайте узнаем как сделать наш код более читаемым, безопасным и надежным.
Что такое async/await?
async/await – это современный способ работы с асинхронным кодом в Swift. Он делает код более читаемым, безопасным и удобным, заменяя сложные вложенные completion handlers или DispatchGroup.
Механизм async/await основан на принципе структурированного параллелизма. Это означает, что асинхронный код выполняется последовательно в рамках своей области видимости, в отличие от замыканий, которые могут разрывать поток выполнения и усложнять его отслеживание.
Пример асинхронного запроса через completion handler:
// Service Layer
func fetchState(callback: @escaping (Result<GameShakeStateDataResponseModel, NetworkError>) -> Void) {
let router = GameShakeRequestFactory.getState
networkService.performRequest(router: router) {(result: Result<GameShakeStateDataResponseModel, NetworkError>) in
switch result {
case .success(let model):
callback(.success(model))
case .failure(let error):
callback(.failure(error))
}
}
}
// Presenter Layer
func fetchState() {
service.fetchState() { [weak self] result in
guard let self else { return }
switch result {
case let .success(response):
updateView(with: response.data)
case let .failure(error):
view?.showErrorToast(with: error.title)
}
}
}
Если переписать код на async/await:
// Service Layer
func fetchState() async throws -> GameShakeStateDataResponseModel {
try await networkService.performRequestAsync(router: GameShakeRequestFactory.getState)
}
// Presenter Layer
func fetchState() {
Task {
do {
let offer = try await service.fetchState()
// handle updateView
} catch {
// handle error
}
}
}
async— объявляет асинхронную функцию;await— приостанавливает выполнение функций до завершения задачи;throws— указывает, что функция может выбрасывать ошибки;Task— запускает асинхронную операцию в синхронном коде.
Читаемость и простота кода
Что хорошо во вложенности в async/await- это то что ее нет.
В то время как в completion handler одна из главных проблем — глубокая вложенность, также известная как callback hell. Когда одна асинхронная операция зависит от другой, completion вложены друг в друга, усложняя чтение и поддержку.
fetchData { result in
switch result {
case .success(let data):
process(data) { processedData in
save(processedData) { success in
if success {
print("Data saved")
}
}
}
case .failure(let error):
print("Error: \\(error)")
}
}
С async/await код остается линейным и чистым:
Task {
do {
let data = try await fetchData()
let processedData = try await process(data)
try await save(processedData)
print("Data saved")
} catch {
print("Error: \\(error)")
}
}
В данном примере наглядно виден принцип структурированного параллелизма. Код выполняется последовательно, строка за строкой. Мы можем ожидать асинхронную функцию до тех пор, пока она не вернет результат (или не выдаст ошибку) и не передаст его следующей функции.
Безопасность потоков
При использовании async/await, большинство асинхронных операций (сетевые запросы, вычисления и т.д) выполняются в фоновом потоке. Однако при необходимости обновления UI, вам нужно явно указать переход на главный поток. Иначе это может привести к сбою приложения. Так как вы пытаетесь изменить интерфейс из фонового потока.
func fetchState() async throws -> GameShakeStateDataResponseModel {
try await networkService.performRequestAsync(router: GameShakeRequestFactory.getState)
}
func fetchState() {
Task {
do {
let offer = try await service.fetchState()
self.view?.updateUI(with: offer) // CRASH!!! (Background thread)
} catch {
// handle error
}
}
}Использование @MainActor гарантирует безопасность при работе с асинхронным кодом. Что также помогает вам избежать проблем с потоками.
func fetchState() {
Task {
do {
let offer = try await service.fetchState()
await MainActor.run {
view?.updateUI(with: offer)
}
} catch {
// handle error
}
}
}
Обработка ошибок
В completion handler ошибки передаются через Result<T, Error> или отдельный параметр Error?. Однако, если забыть вызвать completion, вызывающий код может зависнуть. Это приведет к бесконечной загрузке счетчика или неопределенному состоянию пользовательского интерфейса.
В async/await ошибки всегда требуют обработки:
- Если функция объявлена как
throws, компилятор не позволит пропустить обработку; - Исключение сразу указывает на источник проблемы.
Параллельность и производительность
async/await в Swift работает быстрее, чем старые подходы (замыкания, DispatchGroup, OperationQueue), потому как эффективнее управляет потоками и использует кооперативную многозадачность.
- Нет необходимости вручную управлять потоками или задачами, что предотвращает создание лишних потоков;
- Обеспечена параллельная работа задач без блокировки главного потока. Например, когда операция ожидает завершения при запросах в сети или в работе с диском, остальные операции могут продолжать выполняться;
- Минимизированы накладные расходы на переключение контекста.
Говоря о параллельном выполнений задач, async/await предлагает ряд инструментов (async let, TaskGroup,Task.detached). В данной статье мы остановимся на async let , в сравнений с DispatchGroup.
Вспомним как выглядит код с DispatchGroup:
func fetchOffers() {
let dispatchGroup = DispatchGroup()
dispatchGroup.enter()
forYouService.fetch() { [weak self] result in
// ...
dispatchGroup.leave()
}
dispatchGroup.enter()
loyaltyStateService.fetchLoyaltyState() { [weak self] result in
// ...
dispatchGroup.leave()
}
dispatchGroup.enter()
gameShakeService.fetchGameRules { [weak self] result in
// ...
dispatchGroup.leave()
}
dispatchGroup.notify(queue: .main) { [weak self] in
// handle result
}
}
- Явного вызова
enter()иleave(), что потенциально повышает вероятность ошибок (например, забыв вызватьleave()); - Отслеживания завершения через
notify(), что добавляет дополнительные элементы синхронизации; - Ручной обработки ошибок в каждом замыкании.
async let — это способ параллельного выполнения асинхронных операций.
Он позволяет запустить несколько независимых задач одновременно и затем дождаться их завершения без блокировки. Пример кода:
Task {
async let forYouData = fetchForYou()
async let loyaltyStateData = fetchLoyaltyState()
async let gameRulesData = fetchGameRules()
let (forYouResponse, loyaltyStateResponse, gameRulesResponse) = await (forYouData, loyaltyStateData, gameRulesData)
// handle result
}- Синхронизация скрывается за механизмами асинхронных задач;
- Система сама управляет потоками;
- Ошибки можно обрабатывать централизованно через
do-catch; - Код остается лаконичным и удобным для поддержки.
Итоги
Подводя итоги, хочется отметить следующие преимущества:
- Мы получаем простой и линейный код, где отсутствуют вложенность;
- Мы получаем асинхронный код, который выглядит как синхронный, за счет чего упрощается понимание и дебагинг кода;
- У нас есть четкое разделение между потоками, что снижает количество возникаемых проблем с многопоточностью;
- У нас есть надежная отладка ошибок;
- Также
async/awaitдает современные способы работы с параллельными задачами.
В результате мы получаем чистый, надежный и безопасный код.
Полезные ссылки:
- Презентация async/await на WWDC 21
- Текстовая версия WWDC записи
- Cтатья с описание адаптаций async/await в проекте
- Эволюция async/await