Async await является частью новых структурных изменений параллелизма, которые появились в Swift 5.5 во время WWDC 2021. Параллелизм в Swift означает, что несколько фрагментов кода могут выполняться одновременно. Это очень упрощенное описание, но оно уже должно дать вам представление о том, насколько важен параллелизм в Swift для производительности ваших приложений. С помощью новых асинхронных методов и операторов ожидания мы можем определить методы, выполняющие работу асинхронно.
Возможно, вы уже читали о Swift Concurrency Manifesto Криса Латтнера, о котором было объявлено несколько лет назад. Многие разработчики в сообществе Swift с нетерпением ждут будущего, когда появится структурированный способ определения асинхронного кода. Теперь, когда он, наконец, здесь, мы можем упростить наш код с помощью async-await и сделать наш асинхронный код более удобным для чтения.
Что такое асинхронность?
Async означает асинхронный, и его можно рассматривать как атрибут метода, показывающий, что метод выполняет асинхронную работу. Пример такого метода выглядит следующим образом:
func fetchImages() async throws -> [UIImage] {
// .. perform data request
}
Метод fetchImages определен как асинхронный бросок, что означает, что он выполняет асинхронное задание, которое может привести к сбою. Метод вернет коллекцию изображений, если все пойдет хорошо, или выдаст ошибку, если что-то пошло не так.
Как async заменяет обратные вызовы завершения закрытия
Асинхронные методы заменяют часто встречающиеся обратные вызовы завершения закрытия. Обратные вызовы завершения были обычным явлением в Swift для возврата из асинхронной задачи, часто в сочетании с параметром типа результата. Вышеупомянутый метод был бы написан следующим образом:
func fetchImages(completion: (Result<[UIImage], Error>) -> Void) {
// .. perform data request
}
Определение метода с помощью закрытия завершения все еще возможно в Swift сегодня, но у него есть несколько недостатков, которые решаются с помощью вместо этого использования async:
- Вы должны убедиться, что сами вызываете закрытие завершения в каждом возможном выходе метода. Если этого не сделать, приложение может бесконечно ждать результата.
- Замыкания труднее читать. Рассуждать о порядке выполнения не так просто, как это легко сделать со структурированным параллелизмом.
- Следует избегать циклов сохранения с использованием слабых ссылок.
- Разработчики должны переключать результат, чтобы получить результат. Невозможно использовать операторы try catch на уровне реализации.
Эти недостатки основаны на версии закрытия, использующей относительно новое перечисление Result. Вполне вероятно, что многие проекты все еще используют обратные вызовы завершения без этого перечисления:
func fetchImages(completion: ([UIImage]?, Error?) -> Void) {
// .. perform data request
}
Такое определение метода еще больше усложняет анализ результата на стороне вызывающей стороны. И значение, и ошибка являются необязательными, что требует от нас выполнения развертывания в любом случае. Развертывание этих опций приводит к еще большему беспорядку в коде, что не помогает улучшить читаемость.
Что такое await?
Await — это ключевое слово, которое будет использоваться для вызова асинхронных методов. Вы можете видеть их как лучших друзей в Swift, так как они никогда не обходятся без другого. Вы могли бы в основном сказать:
«Await ожидает обратного вызова от своего приятеля async»
Хоть это и звучит по-детски, но это не ложь! Мы могли бы взглянуть на пример, вызвав наш ранее
определенный асинхронный метод выборки изображений:
do {
let images = try await fetchImages()
print("Fetched \(images.count) images.")
} catch {
print("Fetching images failed with error \(error)")
}
В это трудно поверить, но приведенный выше пример кода выполняет асинхронную задачу. Используя ключевое слово await, мы указываем нашей программе ожидать результата от метода fetchImages и продолжать работу только после получения результата. Это может быть коллекция изображений или ошибка, если что-то пошло не так при получении изображений.
Что такое структурированный параллелизм?
Структурированный параллелизм с вызовами методов async await упрощает определение порядка выполнения. Методы выполняются линейно, без переходов туда и обратно, как в случае с замыканиями.
Чтобы лучше объяснить это, мы можем посмотреть, как мы будем вызывать приведенный выше пример кода до того, как появится структурированный параллелизм:
// 1. Call the method
fetchImages { result in
// 3. The asynchronous method returns
switch result {
case .success(let images):
print("Fetched \(images.count) images.")
case .failure(let error):
print("Fetching images failed with error \(error)")
}
}
// 2. The calling method exits
Как видите, вызывающий метод возвращается до того, как изображения будут получены. В конце концов, результат получен, и мы возвращаемся к нашему потоку в callback. Это неструктурированный порядок выполнения, и ему может быть трудно следовать. Это особенно верно, если мы выполним другой асинхронный метод в нашем обратном вызове завершения, который добавит еще один callback:
// 1. Call the method
fetchImages { result in
// 3. The asynchronous method returns
switch result {
case .success(let images):
print("Fetched \(images.count) images.")
// 4. Call the resize method
resizeImages(images) { result in
// 6. Resize method returns
switch result {
case .success(let images):
print("Decoded \(images.count) images.")
case .failure(let error):
print("Decoding images failed with error \(error)")
}
}
// 5. Fetch images method returns
case .failure(let error):
print("Fetching images failed with error \(error)")
}
}
// 2. The calling method exits
Каждый callback добавляет еще один уровень отступа, что затрудняет соблюдение порядка выполнения.
Переписывание приведенного выше примера кода с использованием async-await лучше всего объясняет, что делает структурированный параллелизм:
do {
// 1. Call the method
let images = try await fetchImages()
// 2. Fetch images method returns
// 3. Call the resize method
let resizedImages = try await resizeImages(images)
// 4. Resize method returns
print("Fetched \(images.count) images.")
} catch {
print("Fetching images failed with error \(error)")
}
// 5. The calling method exits
Порядок выполнения является линейным и, следовательно, легко соблюдается и обосновывается. Понимание асинхронного кода будет проще, пока мы все еще выполняем иногда сложные асинхронные задачи.
Асинхронные методы вызывают функцию, которая не поддерживает параллелизм.
При первом использовании async-await вы можете столкнуться с ошибкой, например:

Эта ошибка возникает, когда мы пытаемся вызвать асинхронный метод из среды синхронных вызовов, которая не поддерживает параллелизм. Мы можем решить эту ошибку определив наш метод fetchData как асинхронный:
func fetchData() async {
do {
try await fetchImages()
} catch {
// .. handle error
}
}
Однако это приведет к перемещению ошибки в другое место. Вместо этого мы могли бы использовать метод Task.init для вызова асинхронного метода из новой задачи, которая поддерживает параллелизм, и присвоить конечный результат свойству в нашей модели представления:
final class ContentViewModel: ObservableObject {
@Published var images: [UIImage] = []
func fetchData() {
Task.init {
do {
self.images = try await fetchImages()
} catch {
// .. handle error
}
}
}
}
Используя async метод с завершающим замыканием, мы создаем среду, в которой можем вызывать асинхронные методы. Метод выборки данных возвращается, как только вызывается async метод, после чего все асинхронные обратные вызовы будут происходить внутри замыкания.
Принятие async-await в существующем проекте
Применяя async-await в существующих проектах, вы должны быть осторожны, чтобы не сломать весь код сразу. При выполнении таких больших рефакторингов полезно подумать о сохранении старых реализаций на время, чтобы вам не пришлось обновлять весь код, прежде чем вы узнаете, достаточно ли стабильна ваша новая реализация. Это похоже на устаревшие методы в SDK, которые используются многими разными разработчиками и проектами.
Очевидно, что вы не обязаны это делать, но это может облегчить опробование async-await в вашем проекте. Кроме того, Xcode упрощает рефакторинг вашего кода, а также предоставляет возможность создать отдельный асинхронный метод:

Каждый метод рефакторинга имеет свою цель и приводит к различным преобразованиям кода. Чтобы лучше понять, как это работает, мы будем использовать следующий код в качестве входных данных для рефакторинга:
struct ImageFetcher {
func fetchImages(completion: @escaping (Result<[UIImage], Error>) -> Void) {
// .. perform data request
}
}
Преобразовать функцию в асинхронную
Первый вариант рефакторинга преобразует метод извлечения изображений в асинхронный вариант без сохранения неасинхронного варианта. Эта опция будет полезна, если вы не хотите поддерживать свою старую реализацию. Полученный код выглядит следующим образом:
struct ImageFetcher {
func fetchImages() async throws -> [UIImage] {
// .. perform data request
}
}
Добавить async альтернативу
Опция добавления альтернативного асинхронного рефакторинга гарантирует сохранение старой реализации, но заботится о добавлении доступного атрибута:
struct ImageFetcher {
@available(*, renamed: "fetchImages()")
func fetchImages(completion: @escaping (Result<[UIImage], Error>) -> Void) {
Task {
do {
let result = try await fetchImages()
completion(.success(result))
} catch {
completion(.failure(error))
}
}
}
func fetchImages() async throws -> [UIImage] {
// .. perform data request
}
}
Доступный атрибут полезен, чтобы знать, где вам нужно обновить свой код для нового варианта параллелизма. Тем не менее, реализация Xcode по умолчанию не содержит никаких предупреждений, поскольку она не помечена как устаревшая. Для этого вам необходимо настроить доступный маркер следующим образом:
@available(*, deprecated, renamed: "fetchImages()")
Вы можете узнать больше о маркере доступности в моей статье Как использовать атрибут #available в Swift.
Преимущество использования этого варианта рефакторинга заключается в том, что он позволяет постепенно адаптироваться к новым структурным изменениям параллелизма без необходимости сразу преобразовывать весь проект. Промежуточное построение полезно, так как вы знаете, что изменения вашего кода работают должным образом. Реализации, использующие старый метод, получат следующее предупреждение:

Вы можете постепенно изменять свои реализации по всему проекту и использовать предоставленную кнопку исправления в Xcode для автоматического преобразования кода для использования новой реализации.
Добавить async оболочку
Последний метод рефакторинга приведет к самому простому преобразованию, поскольку он просто использует ваш существующий код:
struct ImageFetcher {
@available(*, renamed: "fetchImages()")
func fetchImages(completion: @escaping (Result<[UIImage], Error>) -> Void) {
// .. perform data request
}
func fetchImages() async throws -> [UIImage] {
return try await withCheckedThrowingContinuation { continuation in
fetchImages() { result in
continuation.resume(with: result)
}
}
}
}
Недавно добавленный метод использует метод withCheckedThrowingContinuation, который был введен в Swift для преобразования методов на основе замыкания без особых усилий. Методы без выдачи могут использовать withCheckedContinuation, который работает так же, но не поддерживает выдачу ошибок.
Эти два метода приостанавливают текущую задачу до тех пор, пока не будет вызвано данное закрытие, чтобы вызвать продолжение метода асинхронного ожидания. Другими словами: вы должны убедиться, что закрытие продолжения вызывается на основе обратного вызова вашего собственного метода, основанного на замыкании. В нашем примере это сводится к вызову продолжения с нашим значением результата, возвращаемым исходным обратным вызовом выборки изображений.
Выбор правильного метода рефакторинга async-await для вашего проекта
Этих трех вариантов рефакторинга должно быть достаточно, чтобы преобразовать ваш существующий код в асинхронные альтернативы. В зависимости от размера вашего проекта и количества времени, которое у вас есть на рефакторинг, вы можете выбрать другой вариант рефакторинга. Тем не менее, я бы настоятельно рекомендовал применять изменения постепенно, поскольку это позволяет изолировать измененные части, облегчая проверку того, работают ли ваши изменения должным образом.
Устранение ошибки «Reference to captured parameter ‘self’» в параллельно выполняющемся коде»
Еще одна распространенная ошибка при работе с асинхронными методами:
«Reference to captured parameter ‘self’» в параллельно выполняющемся коде»
В основном это означает, что мы пытаемся сослаться на неизменяемый экземпляр self. Другими словами, вы, скорее всего, ссылаетесь либо на неизменяемое свойство, либо на экземпляр, например структуру, как в следующем примере:

Эту ошибку можно исправить, сделав ваши свойства изменяемыми или изменив структуру на ссылочный тип, например класс.
Будет ли async await концом перечисления Result?
Мы видели, что async методы заменяют асинхронные методы, использующие обратные вызовы закрытия. Мы могли бы спросить себя, будет ли это концом перечисления Result в Swift. В конце концов, они нам больше не нужны, поскольку мы можем использовать операторы try-catch в сочетании с async-await.
Перечисление Result не исчезнет в ближайшее время, поскольку оно все еще используется во многих местах в проектах Swift. Тем не менее, я не удивлюсь, увидев, что он устарел, как только скорость принятия async-await становится выше. Лично я не использовал перечисление Result ни в каком другом месте, кроме обратных вызовов завершения. Как только я полностью использую async-await, я больше не буду использовать перечисление.
Продолжаем свое путешествие в Swift Concurrency
Изменения параллелизма — это больше, чем просто асинхронное ожидание, они включают в себя множество новых функций, которые вы можете использовать в своем коде. Теперь, когда вы узнали об основах async и await, пришло время погрузиться в другие новые функции параллелизма:
- Tasks in Swift explained with code examples
- Async await in Swift explained with code examples
- Sendable and @Sendable closures explained with code examples
- Nonisolated and isolated keywords: Understanding Actor isolation
- Async let explained: call async functions in parallel
- MainActor usage in Swift explained to dispatch to the main thread
- Actors in Swift: how to use and prevent data races
Заключение
Async-await в Swift допускает структурированный параллелизм, что улучшает читаемость сложного асинхронного кода. Замыкания завершения больше не нужны, а вызов нескольких асинхронных методов друг за другом стал намного более читабельным. Могут возникать несколько новых типов ошибок, которые можно решить, убедившись, что асинхронные методы вызываются из функции, поддерживающей параллелизм, и при этом не изменяются какие-либо неизменяемые ссылки.
Если вы хотите узнать больше советов по Swift, посетите страницу категории Swift. Не стесняйтесь обращаться ко мне или писать мне в Твиттере, если у вас есть какие-либо дополнительные советы или отзывы.
Спасибо!