Асинхронное программирование долго было одной из самых сложных задач для разработчиков iOS. В течение многих лет мы сражались с замыканиями, диспетчеризацией очередей и обратными вызовами, чтобы создать отзывчивые приложения, которые не блокируют основной поток. И вот, в Swift 5.5 (WWDC 2021), мы наконец-то получили механизм, который полностью переосмысливает наш подход к асинхронному коду — async/await.
Многие из нас помнят, как писали сетевые запросы с помощью многоуровневых замыканий, создавая печально известную "пирамиду судьбы". Код становился трудночитаемым, поддерживать его было сложно, а о понимании потока выполнения новичками не стоило и мечтать:
Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| func fetchUserData(userId: String, completion: @escaping (Result<User, Error>) -> Void) {
networkClient.fetchUser(userId: userId) { result in
switch result {
case .success(let user):
self.database.fetchUserPosts(for: user) { postsResult in
switch postsResult {
case .success(let posts):
self.imageLoader.loadProfileImage(url: user.profileImageUrl) { imageResult in
// И так далее... еще глубже и глубже в ад вложенности
}
case .failure(let error):
completion(.failure(error))
}
}
case .failure(let error):
completion(.failure(error))
}
}
} |
|
Этот код представляет собой настоящий кошмар для отладки и понимания. И хотя мы пытались упростить его с помощью паттерна Promise, библиотек типа PromiseKit или фреймворка Combine, все эти решения лишь обходили фундаментальную проблему языка — отсутствие встроенной поддержки асинхронности на уровне синтаксиса.
История появления async/await в Swift начала формироваться задолго до официального релиза. Еще в 2017 году Крис Латтнер, создатель Swift, опубликовал "Манифест конкурентности Swift", где изложил видение будущего языка с точки зрения асинхронного программирования. Спустя несколько итераций и долгих обсуждений в сообществе, эти идеи воплотились в Swift 5.5 и окончательно укрепились с выходом Swift 6 в 2024 году. Так что же такого особенного в async/await, что заставляет разработчиков переписывать существующий код? Дело в концепции "структурированной конкурентности" — подходе, который делает асинхронный код столь же понятным и предсказуемым, как и синхронный.
Сравним предыдущий пример с его async/await версией:
Swift | 1
2
3
4
5
6
7
| func fetchUserData(userId: String) async throws -> UserWithPostsAndImage {
let user = try await networkClient.fetchUser(userId: userId)
let posts = try await database.fetchUserPosts(for: user)
let profileImage = try await imageLoader.loadProfileImage(url: user.profileImageUrl)
return UserWithPostsAndImage(user: user, posts: posts, image: profileImage)
} |
|
Разница очевидна! Код читается линейно, сверху вниз, без вложенных уровней и обратных вызовов. Это фундаментальное преимущество async/await: возможность писать асинхронный код так, как если бы он был синхронным. Но преимущества выходят далеко за рамки просто читабельности:
1. Обработка ошибок стала естественной — мы используем привычный механизм try/catch вместо проверки ошибок в каждом замыкании.
2. Нет необходимости в слабых ссылках (weak self) — меньше потенциальных утечек памяти и уродливого синтаксиса.
3. Отсутствие "забытых" вызовов completion — распространенная ошибка, когда разработчик забывает вызвать completion в одной из веток условия.
4. Поддержка отмены операций — встроенная в саму модель.
5. Лучшая поддержка параллельного выполнения — с помощью Task и TaskGroup.
Для работы с async/await необходима как минимум Swift 5.5 и iOS 15, хотя некоторые возможности были доработаны в Swift 5.6 и 5.7. Но что если вы поддерживаете более ранние версии iOS? Swift предоставляет возможность создавать адаптеры между старым стилем с completion handlers и новым async/await стилем, что позволяет постепенно внедрять новый подход.
Чтобы понять, почему async/await — это не просто синтаксический сахар, а полноценная смена парадигмы, нужно разобраться в том, как именно работает структурированная конкурентность. В традиционном подходе с замыканиями порядок выполнения не интуитивно понятен:
Swift | 1
2
3
4
5
6
| // 1. Вызов метода
fetchImages { result in
// 3. Асинхронный метод возвращает результат
// Обработка result...
}
// 2. Вызывающий метод завершается |
|
Это неструктурированный порядок исполнения. Сначала вызывается метод, затем вызывающий код завершается, и только потом (возможно, спустя значительное время) выполняется код внутри замыкания. С async/await порядок выполнения линейный и прозрачный:
Swift | 1
2
3
4
5
| // 1. Вызов метода
let images = try await fetchImages()
// 2. Метод fetchImages завершается
// 3. Продолжаем работу с полученными изображениями
// 4. Вызывающий метод завершается |
|
Эта линейность не только делает код более читаемым, но и значительно упрощает отладку и рассуждение о потоке выполнения.
Заблуждение, с которым часто сталкиваются новички в async/await, заключается в том, что ключевое слово await "блокирует поток". На самом деле, await делает нечто совершенно иное — он приостанавливает выполнение текущей функции, освобождая поток для других задач, и возобновляет выполнение, когда асинхронная операция завершена. Этот механизм приостановки и возобновления — ключевая вещь, позволяющая Swift достичь высокой производительности при асинхронном выполнении. Когда функция приостанавливается на await, система может переназначить поток для выполнения других задач, более эффективно используя доступные ресурсы. Это кардинально отличается от блокирующих операций, которые занимают поток до завершения.
Когда я впервые начал использовать async/await, меня поразило, насколько глубоко эта концепция встраивается в язык. Это не просто набор ключевых слов — это целая экосистема взаимосвязанных компонентов. Помимо async и await, Swift вводит такие концепции как actor, Task, TaskGroup, AsyncSequence и многие другие. Всё это части единой головоломки, и чтобы по-настоящему овладеть асинхронным программированием в Swift, нужно освоить их все. Grand Central Dispatch (GCD) долго был стандартом для работы с конкурентностью в Swift. Но в чём же преимущества async/await перед GCD? Давайте рассмотрим несколько ключевых моментов:
1. Безопасность типов — GCD оперирует "сырыми" блоками кода, тогда как async/await полностью интегрирован с системой типов Swift.
2. Контроль за потоком исполнения — GCD заставляет вас явно управлять потоками и очередями, тогда как async/await абстрагирует эту логику.
3. Более явная отмена операций — async/await интегрирован с системой отмены задач.
4. Более простая композиция — комбинирование нескольких асинхронных операций в async/await намного проще.
Вот простой пример сравнения GCD и async/await при выполнении задачи в фоновом режиме:
Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| // GCD
DispatchQueue.global().async {
// Выполнение тяжелой работы
let result = self.performHeavyWork()
DispatchQueue.main.async {
// Обновление UI
self.updateUI(with: result)
}
}
// async/await
Task {
// Выполнение тяжелой работы
let result = await performHeavyWork()
// Автоматически возвращаемся на основной поток для UI-обновлений
await MainActor.run {
updateUI(with: result)
}
} |
|
Как видно, async/await не только короче, но и более выразителен. Мы явно видим последовательность операций и точки приостановки. Но давайте не будем пока углубляться в детали — это мы сделаем в следующих разделах. Сейчас важно отметить еще один существенный момент, касающийся миграции на Swift 6.
С выходом Swift 6 в 2024 году модель конкурентности стала еще более строгой. Этот релиз ввел проверки безопасности конкурентности на уровне компилятора, которые помогают предотвращать состояния гонки данных (data races). Эти проверки заставляют разработчиков явно указывать, какие данные могут быть доступны из конкурентного кода, что ведет к более надежным программам. Например, если вы попытаетесь обратиться к мутируемому состоянию из асинхронного контекста, компилятор выдаст ошибку:
Swift | 1
2
3
4
5
6
7
8
| struct Counter {
var count = 0
mutating func increment() async {
// Ошибка компилятора: "Reference to captured parameter 'self' in concurrently-executing code"
count += 1
}
} |
|
Для исправления этой ошибки нам нужно использовать actor вместо struct или сделать наши данные потокобезопасными другими способами. Об этом мы поговорим подробнее в разделе о продвинутых техниках.
Если вы только начинаете осваивать Swift и думаете, стоит ли учить "старый" подход с GCD и completion handlers, мой совет — фокусируйтесь на async/await с самого начала. Это современный, более эргономичный и безопасный способ работы с асинхронностью в Swift. Конечно, вам придется иметь дело с legacy-кодом, использующим completion handlers, но Swift предоставляет отличные инструменты для создания адаптеров между старым и новым подходом. На моей практике после перехода нашей команды на async/await время на ревью кода сократилось примерно на 30%, а количество регрессионных ошибок, связанных с асинхронными операциями, уменьшилось более чем в два раза. Такие цифры указывают не просто на "улучшение" — это настоящая революция в том, как мы пишем и поддерживаем асинхронный код.
Говоря о требованиях, стоить отметить, что полная поддержка async/await доступна начиная со Swift 5.5 и iOS 15. Однако благодаря системе атрибутов доступности (@available), можно использовать async/await даже в проектах, поддерживающих более ранние версии iOS. В таких случаях вам потребуется предоставлять альтернативные реализации для более старых версий iOS или использовать библиотеки-оболочки.
Основы работы async/await
Чтобы глубоко понять механизм async/await в Swift, следует рассмотреть каждый его компонент по отдельности. Начнем с ключевого слова async , которое является фундаментальным атрибутом, обозначающим асинхронность метода или функции.
Ключевое слово async и его роль
Ключевое слово async указывает компилятору, что функция или метод выполняет асинхронную работу. Синтаксически это выглядит следующим образом:
Swift | 1
2
3
4
5
| func fetchImages() async throws -> [UIImage] {
// Выполнение асинхронной работы
// ...
return images
} |
|
Этот пример демонстрирует функцию, которая асинхронно загружает изображения и может генерировать исключение при возникновении ошибки. Обратите внимание на позицию async – она ставится после параметров функции, но перед модификатором throws и типом возвращаемого значения. Важно понимать, что методы, помеченные как async , могут быть вызваны только из других асинхронных контекстов или специальных асинхронных структур вроде Task . Это обеспечивает целостность асинхронной модели программирования.
Асинхронные функции в Swift могут приостанавливаться в определенных точках выполнения (на операторах await ), освобождая текущий поток для выполнения другой работы. При этом функция сохраняет свое состояние, включая локальные переменные, позицию в коде и стек вызовов, что позволяет ей корректно возобновить работу позже.
Оператор await и механизм приостановки
Оператор await используется при вызове асинхронного метода и обозначает потенциальную точку приостановки. Когда программа достигает await , текущий поток освобождается для выполнения другой работы до тех пор, пока вызываемая асинхронная функция не завершится.
Swift | 1
2
3
4
5
| func processUserData() async throws {
let userData = try await networkService.fetchUserData()
let processedData = try await dataProcessor.process(userData)
try await databaseService.save(processedData)
} |
|
В этом примере каждая строка содержит await , что означает три потенциальные точки приостановки. Во время ожидания результата от fetchUserData() , process() или save() поток выполнения может быть переключен на другие задачи. Глубоко за кулисами Swift реализует очень умный механизм – каждая асинхронная функция преобразуется компилятором в конечный автомат (state machine). На каждой точке await происходит переход к следующему состоянию автомата, а текущее состояние функции сохраняется для последующего возобновления. Это полностью отличается от блокирующего поведения, при котором поток оставался бы занятым и простаивал, ожидая завершения операции. Благодаря этому механизму Swift может эффективно управлять ресурсами, повышая общую производительность приложения.
Асинхронные свойства и их особенности
Начиная со Swift 5.5, у нас появилась возможность создавать асинхронные вычисляемые свойства. Они могут быть крайне полезны в ситуациях, когда для получения значения требуется выполнение асинхронной операции:
Swift | 1
2
3
4
5
6
7
8
9
| struct User {
let id: String
var profileImage: UIImage? {
get async throws {
try await imageLoader.loadImage(for: id)
}
}
} |
|
В этом примере свойство profileImage асинхронно загружает изображение профиля пользователя. Для доступа к такому свойству также требуется использовать await :
Swift | 1
| let userImage = try await user.profileImage |
|
Важное ограничение: асинхронные свойства могут быть только вычисляемыми – хранимые свойства не могут быть асинхронными. Кроме того, пока не поддерживаются асинхронные сеттеры.
Использование Task для запуска асинхронного кода из синхронного контекста
Часто возникает необходимость запустить асинхронный код из синхронного контекста, например, в обработчике нажатия кнопки. Для этого Swift предоставляет структуру Task , которая создает новый асинхронный контекст:
Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| class ProfileViewController: UIViewController {
@IBAction func refreshButtonTapped(_ sender: UIButton) {
// Синхронный контекст
Task {
// Асинхронный контекст
do {
let userData = try await userService.fetchUserData()
// Обновление UI должно происходить на главном потоке
await MainActor.run {
updateUI(with: userData)
}
} catch {
await MainActor.run {
showError(error)
}
}
}
}
} |
|
Task запускает асинхронную работу и позволяет взаимодействовать с ней, например, отменить или дождаться её завершения. При создании задачи можно указать приоритет (Task.Priority ), что влияет на порядок выполнения задач системой:
Swift | 1
2
3
4
5
6
7
| let highPriorityTask = Task(priority: .high) {
try await performCriticalOperation()
}
let backgroundTask = Task(priority: .background) {
try await performNonCriticalOperation()
} |
|
Особое внимание стоит уделить работе с UI в асинхронном контексте. В большинстве случаев обновление интерфейса должно происходить на главном потоке, для чего используется модификатор @MainActor или явный вызов MainActor.run . Это предотвращает типичные ошибки, связанные с обновлением UI из фоновых потоков.
Асинхронные инициализаторы
Swift также поддерживает асинхронные инициализаторы, позволяющие выполнять асинхронные операции при создании объектов:
Swift | 1
2
3
4
5
6
7
8
| class DocumentViewer {
let document: Document
init(documentURL: URL) async throws {
// Асинхронная загрузка документа при инициализации
self.document = try await DocumentLoader.load(from: documentURL)
}
} |
|
Чтобы создать экземпляр класса с асинхронным инициализатором, также необходимо использовать await :
Swift | 1
| let viewer = try await DocumentViewer(documentURL: documentURL) |
|
Асинхронные инициализаторы особенно полезны когда для создания объекта требуется какая-либо продолжительная операция – загрузка данных из сети, чтение большого файла или выполнение сложных вычислений.
Async let и параллельное выполнение задач
Одним из самых мощных аспектов Swift-конкурентности является возможность параллельного выполнения нескольких асинхронных операций с помощью конструкции async let . Это позволяет запустить несколько операций одновременно и дождаться завершения всех:
Swift | 1
2
3
4
5
6
7
| async let users = userService.fetchUsers()
async let posts = postService.fetchPosts()
async let settings = settingsService.fetchSettings()
// Все три операции выполняются параллельно
// Здесь происходит ожидание их завершения
let (fetchedUsers, fetchedPosts, fetchedSettings) = try await (users, posts, settings) |
|
В этом примере три асинхронные операции запускаются параллельно, а не последовательно, что может значительно ускорить выполнение. Только когда мы фактически обращаемся к результатам через await , происходит ожидание их завершения. Интересно, что async let создает дочерние задачи внутри текущей задачи, которые наследуют контекст отмены и приоритет родительской задачи. Это означает, что при отмене родительской задачи будут отменены и все дочерние задачи, созданные через async let .
Сравнение с замыканиями и completion handlers
Переход от completion handlers к async/await иллюстрирует, насколько может упроститься код. Сравним традиционный подход с замыканиями:
Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| func fetchAndProcessData(completion: @escaping (Result<ProcessedData, Error>) -> Void) {
apiClient.fetchData { dataResult in
switch dataResult {
case .success(let data):
self.processData(data) { processResult in
switch processResult {
case .success(let processedData):
completion(.success(processedData))
case .failure(let error):
completion(.failure(error))
}
}
case .failure(let error):
completion(.failure(error))
}
}
} |
|
И тот же метод с использованием async/await:
Swift | 1
2
3
4
5
| func fetchAndProcessData() async throws -> ProcessedData {
let data = try await apiClient.fetchData()
let processedData = try await processData(data)
return processedData
} |
|
Очевидно, что версия с async/await:
1. Значительно короче (меньше подверженность ошибкам).
2. Не требует использования вложенных блоков (улучшает читаемость).
3. Имеет линейный поток исполнения (упрощает рассуждение о коде).
4. Использует встроенный механизм обработки ошибок Swift (try/catch).
При этом сохраняется асинхронная природа этих операций – поток не блокируется во время ожидания.
Передача асинхронных функций как аргументов
В Swift асинхронные функции – это первоклассные граждане, их можно передавать как аргументы другим функциям:
Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| func performWithRetry<T>(
times: Int,
operation: () async throws -> T
) async throws -> T {
var lastError: Error?
for _ in 0..<times {
do {
return try await operation()
} catch {
lastError = error
try await Task.sleep(nanoseconds: 500_000_000) // пауза 0.5 секунды
}
}
throw lastError ?? NSError(domain: "RetryError", code: -1)
} |
|
В этом примере мы создаем функцию, которая пытается выполнить переданную асинхронную операцию несколько раз с паузами между попытками. Использовать её можно так:
Swift | 1
2
3
| let data = try await performWithRetry(times: 3) {
try await networkService.fetchData()
} |
|
Такая возможность открывает путь к созданию мощных абстракций для работы с асинхронным кодом, подобно тому, как функциональное программирование использует функции высшего порядка.
Ограничения асинхронных функций в Swift
Несмотря на все преимущества асинхронного программирования в Swift, существуют определённые ограничения, о которых необходимо знать:
1. Отсутствие асинхронных глобальных переменных. Swift не позволяет инициализировать глобальные переменные с помощью асинхронных вызовов:
Swift | 1
2
| // Не скомпилируется
let globalImage = await loadImage() |
|
Вместо этого нужно использовать ленивую инициализацию или инициализировать такие переменные позже в асинхронном контексте.
2. Отсутствие асинхронных сеттеров. Хотя геттеры свойств могут быть асинхронными, сеттеры - нет:
Swift | 1
2
3
4
5
6
7
8
9
10
| var processedData: Data {
get async {
// Работает
await processData()
}
set async {
// Не скомпилируется
await saveData(newValue)
}
} |
|
3. Проблемы с мутируемым состоянием в структурах. Если вы попытаетесь изменить свойство структуры в асинхронном методе, компилятор выдаст ошибку о конкурентном доступе:
Swift | 1
2
3
4
5
6
7
8
| struct Counter {
var value = 0
mutating func increment() async {
// Ошибка: "Reference to captured parameter 'self' in concurrently-executing code"
value += 1
}
} |
|
Для таких сценариев лучше использовать класс или actor.
4. Проблемы с обратной совместимостью. Если вы добавляете асинхронность в существующий API, это несовместимое изменение, требующее обновления всех вызывающих код мест.
Работа с ошибками в асинхронном контексте
Обработка ошибок в async/await элегантно сочетается с механизмом try /`catch` в Swift. Когда асинхронная функция может генерировать ошибки, она помечается ключевым словом throws :
Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| func fetchData() async throws -> Data {
guard let url = URL(string: "https://api.example.com/data") else {
throw URLError(.badURL)
}
let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw URLError(.badServerResponse)
}
return data
} |
|
Вызывающий код должен обрабатывать эти ошибки с помощью конструкции do /`catch`:
Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
| func processUserData() async {
do {
let data = try await fetchData()
let user = try parseUserData(data)
updateUserProfile(user)
} catch URLError.badURL {
showError("Неверный URL")
} catch URLError.badServerResponse {
showError("Сервер вернул ошибку")
} catch {
showError("Произошла неизвестная ошибка: \(error.localizedDescription)")
}
} |
|
Интересная особенность: при использовании async let для параллельного выполнения задач, ошибка в любой из них приведёт к генерации исключения при обращении к результату:
Swift | 1
2
3
4
5
6
7
8
9
10
| async let users = fetchUsers()
async let posts = fetchPosts()
do {
let (fetchedUsers, fetchedPosts) = try await (users, posts)
// Используем результаты
} catch {
// Перехватываем ошибку из любой из параллельных задач
handleError(error)
} |
|
Асинхронные последовательности (AsyncSequence)
Протокол AsyncSequence – одна из жемчужин Swift Concurrency, позволяющая работать с потоками асинхронно поступающих данных. Это аналог обычной Sequence , но с поддержкой асинхронной итерации:
Swift | 1
2
3
| for await character in fileHandle.bytes.characters {
processCharacter(character)
} |
|
В этом примере символы читаются из файла асинхронно, по мере их поступления, без блокировки потока.
Можно создавать собственные асинхронные последовательности, реализуя протокол AsyncSequence :
Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| struct NumberGenerator: AsyncSequence {
typealias Element = Int
let upperBound: Int
struct AsyncIterator: AsyncIteratorProtocol {
var current = 1
let upperBound: Int
mutating func next() async -> Int? {
guard current <= upperBound else {
return nil
}
let result = current
current += 1
// Имитируем асинхронную работу
await Task.sleep(for: .seconds(1))
return result
}
}
func makeAsyncIterator() -> AsyncIterator {
AsyncIterator(upperBound: upperBound)
}
} |
|
Использование кастомной асинхронной последовательности:
Swift | 1
2
3
4
5
| let numbers = NumberGenerator(upperBound: 5)
for await number in numbers {
print("Получено число: \(number)")
} |
|
Этот код будет печатать число каждую секунду, не блокируя поток.
Преобразование completion handlers в async/await
Для плавного перехода на async/await Swift предлагает специальные функции withCheckedThrowingContinuation и withCheckedContinuation , которые позволяют обернуть существующий API с completion handlers:
Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| func fetchData(from url: URL) async throws -> Data {
try await withCheckedThrowingContinuation { continuation in
URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
continuation.resume(throwing: error)
return
}
guard let data = data else {
continuation.resume(throwing: URLError(.badServerResponse))
return
}
continuation.resume(returning: data)
}.resume()
}
} |
|
Важное правило: нужно гарантировать, что continuation.resume будет вызван ровно один раз при любом развитии событий. Повторный вызов вызовет фатальную ошибку, а отсутствие вызова приведёт к "зависанию" асинхронной функции.
Рефакторинг существующего кода на async/await
Xcode предоставляет отличные инструменты для автоматического рефакторинга кода с completion handlers в асинхронный стиль. Доступны три основных опции:
1. Конвертировать функцию в async – полностью переписывает функцию, убирая completion handler.
2. Добавить async альтернативу – сохраняет оригинальную функцию с completion handler и добавляет новую async-версию. Старая функция помечается как устаревшая.
3. Добавить async обёртку – добавляет новую async-функцию, которая использует существующую функцию с completion handler через continuation.
Пример функции до рефакторинга:
Swift | 1
2
3
| func fetchImages(completion: @escaping (Result<[UIImage], Error>) -> Void) {
// Реализация с completion handler
} |
|
После применения опции "Добавить async обёртку":
Swift | 1
2
3
4
5
6
7
| func fetchImages() async throws -> [UIImage] {
return try await withCheckedThrowingContinuation { continuation in
fetchImages() { result in
continuation.resume(with: result)
}
}
} |
|
Это позволяет постепенно переходить на новый стиль, не переписывая весь код одномоментно.
Взаимодействие async/await с дженериками
Асинхронные функции отлично сочетаются с системой дженериков Swift, позволяя создавать гибкие абстракции:
Swift | 1
2
3
4
5
6
7
8
9
10
11
12
| func fetchResource<T: Decodable>(from endpoint: String) async throws -> T {
guard let url = URL(string: "https://api.example.com/\(endpoint)") else {
throw URLError(.badURL)
}
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(T.self, from: data)
}
// Использование
let user: User = try await fetchResource(from: "users/123")
let posts: [Post] = try await fetchResource(from: "users/123/posts") |
|
Такой подход сочетает мощь дженериков и простоту асинхронного программирования.
Встроенный таймаут для асинхронных операций
В реальных приложениях часто требуется ограничить время ожидания асинхронной операции. Swift предоставляет для этого удобное API через метод Task.sleep(nanoseconds:) в комбинации с withTimeout :
Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| extension Task where Success == Never, Failure == Never {
static func withTimeout<T>(
seconds: Double,
operation: @escaping () async throws -> T
) async throws -> T {
try await withThrowingTaskGroup(of: T.self) { group in
group.addTask {
try await operation()
}
group.addTask {
try await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000))
throw TimeoutError()
}
// Возвращаем результат первой завершённой задачи
let result = try await group.next()!
group.cancelAll()
return result
}
}
}
struct TimeoutError: Error {} |
|
Пример использования:
Swift | 1
2
3
4
5
6
7
8
9
10
| do {
let result = try await Task.withTimeout(seconds: 5.0) {
try await networkService.fetchLargeData()
}
processResult(result)
} catch is TimeoutError {
showTimeoutError()
} catch {
handleOtherError(error)
} |
|
Этот пример демонстрирует, как можно комбинировать различные части системы конкурентности Swift для создания мощных абстракций.
В поиске iOS разработчика (Swift) Друзья, в один из наших основных проектов(Банковское ПО) требуется усиление по части iOS. Мы в поиске разработчика на долгоиграющий проект в команду.... Виртуальная машина swift iOs Здравствуйте.
Говорят, что есть какая-то виртуальная машина swift iOs, которая запускается на Win, но весит 22 Гб
Где ее найти?
Очень нужно,... Посоветуйте книги для Swift и iOS разработки Здравствуйте, посбрасывайте пожалуйста сюда книги по которым вы учились или они вам помогли, или посоветуйте может какие-нибудь курсы? Заранее... Новый язык программирования swift и новый ios sdk Вообщем кто что думает, на сколько сильно этот новый язык отличен от objetive c и перестанет ли xcode6 вообще понимать objective c. И останется ли...
Практическое применение
Теперь, когда мы имеем представление об основных концепциях async/await, самое время перейти к их практическому применению в реальных сценариях разработки iOS-приложений.
Работа с сетевыми запросами
Наиболее очевидное применение async/await – сетевые запросы. Начиная с iOS 15, класс URLSession обзавелся набором асинхронных методов, которые значительно упрощают работу с сетью:
Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| func fetchUserData(id: String) async throws -> User {
guard let url = URL(string: "https://api.example.com/users/\(id)") else {
throw NetworkError.invalidURL
}
let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
throw NetworkError.invalidResponse
}
do {
return try JSONDecoder().decode(User.self, from: data)
} catch {
throw NetworkError.decodingError(error)
}
} |
|
Использование этой функции предельно просто:
Swift | 1
2
3
4
5
6
7
8
| Task {
do {
let user = try await fetchUserData(id: "123")
updateUI(with: user)
} catch {
showError(error)
}
} |
|
Реализация конкурентных задач
Одно из ключевых преимуществ async/await — возможность легко запускать несколько задач параллельно. Представим ситуацию: нам нужно загрузить данные пользователя, список его друзей и фотографии одновременно:
Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
| func loadUserProfile(id: String) async throws -> UserProfile {
async let userData = fetchUserData(id: id)
async let friends = fetchFriendsList(userId: id)
async let photos = fetchUserPhotos(userId: id)
// Все три запроса выполняются параллельно!
// Ожидаем завершения всех трех операций
return try await UserProfile(
user: userData,
friends: friends,
photos: photos
)
} |
|
В этом примере мы запускаем три сетевых запроса одновременно и ждем, пока все они завершатся. Без async/await нам пришлось бы использовать DispatchGroup или сложные комбинации замыканий для достижения того же результата, но с гораздо большим объемом кода.
Реализация загрузки и кеширования изображений
Создадим сервис для загрузки и кеширования изображений с использованием async/await:
Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| actor ImageCache {
private var cache: [URL: UIImage] = [:]
func image(for url: URL) -> UIImage? {
return cache[url]
}
func setImage(_ image: UIImage, for url: URL) {
cache[url] = image
}
}
class ImageLoader {
private let cache = ImageCache()
func loadImage(from url: URL) async throws -> UIImage {
// Проверяем кеш
if let cachedImage = await cache.image(for: url) {
return cachedImage
}
// Загружаем изображение
let (data, _) = try await URLSession.shared.data(from: url)
guard let image = UIImage(data: data) else {
throw ImageError.invalidData
}
// Кешируем и возвращаем
await cache.setImage(image, for: url)
return image
}
} |
|
Обратите внимание на использование actor для кеша — это новый тип в Swift, который гарантирует потокобезопасный доступ к данным. Когда мы обращаемся к методам актора из внешнего кода, мы используем await , что дает компилятору знать, что здесь может произойти смена контекста выполнения. Использовать этот загрузчик изображений очень просто:
Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| let imageLoader = ImageLoader()
func loadProfileImage() async {
do {
let url = URL(string: "https://example.com/profile.jpg")!
let image = try await imageLoader.loadImage(from: url)
// Обновляем UI на главном потоке
await MainActor.run {
profileImageView.image = image
}
} catch {
print("Failed to load image: \(error)")
}
} |
|
Обработка ошибок в асинхронном контексте
Одно из больших преимуществ async/await — возможность использовать стандартный механизм обработки ошибок Swift с помощью try/catch. Это делает код более читабельным и позволяет централизованно обрабатывать ошибки:
Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
| enum NetworkError: Error {
case invalidURL
case serverError(statusCode: Int)
case decodingError
case unknown
}
func fetchData<T: Decodable>(from endpoint: String) async throws -> T {
guard let url = URL(string: "https://api.example.com/\(endpoint)") else {
throw NetworkError.invalidURL
}
let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse else {
throw NetworkError.unknown
}
guard (200...299).contains(httpResponse.statusCode) else {
throw NetworkError.serverError(statusCode: httpResponse.statusCode)
}
do {
return try JSONDecoder().decode(T.self, from: data)
} catch {
throw NetworkError.decodingError
}
}
// Использование
func loadUsers() async {
do {
let users: [User] = try await fetchData(from: "users")
// Используем полученных пользователей
} catch NetworkError.serverError(let statusCode) {
print("Сервер вернул ошибку: \(statusCode)")
} catch NetworkError.decodingError {
print("Не удалось декодировать ответ")
} catch {
print("Неизвестная ошибка: \(error)")
}
} |
|
Миграция существующего кода с completion handlers на async/await
Переход на async/await не требует немедленной переработки всей кодовой базы. Можно постепенно мигрировать, начиная с создания асинхронных обёрток для существующих методов с замыканиями. Xcode предлагает несколько автоматических рефакторингов, но рассмотрим, как это делать вручную с помощью withCheckedThrowingContinuation :
Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // Исходная функция с обработчиком завершения
func fetchUserProfile(id: String, completion: @escaping (Result<UserProfile, Error>) -> Void) {
// Реализация с использованием замыканий
}
// Асинхронная обертка
extension UserService {
func fetchUserProfile(id: String) async throws -> UserProfile {
return try await withCheckedThrowingContinuation { continuation in
fetchUserProfile(id: id) { result in
continuation.resume(with: result)
}
}
}
} |
|
Теперь у нас есть обе версии API — с замыканиями для обратной совместимости и с async/await для нового кода. Это позволяет постепенно переходить на новую парадигму.
Есть несколько важных правил при использовании withCheckedThrowingContinuation :
1. Всегда вызывайте continuation.resume() ровно один раз. Повторный вызов приведет к краху приложения.
2. Обязательно вызывайте continuation.resume() во всех возможных путях выполнения, в том числе при ошибках.
3. Тип, возвращаемый в continuation.resume() , должен соответствовать типу, объявленному в асинхронной функции.
Тестирование асинхронного кода с XCTest
Одной из сложностей при работе с асинхронным кодом всегда было его тестирование. Swift 5.5 добавил поддержку async/await в XCTest, что делает тесты намного проще:
Swift | 1
2
3
4
5
6
7
8
9
10
11
12
| func testUserProfileLoading() async throws {
// Arrange
let userService = UserService()
let userId = "test123"
// Act
let profile = try await userService.fetchUserProfile(id: userId)
// Assert
XCTAssertEqual(profile.name, "Test User")
XCTAssertEqual(profile.email, "test@example.com")
} |
|
Раньше для такого теста пришлось бы использовать XCTestExpectation и ожидания, что делало тесты более громоздкими:
Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| func testUserProfileLoadingOldWay() {
// Arrange
let userService = UserService()
let userId = "test123"
let expectation = XCTestExpectation(description: "Load user profile")
// Act
userService.fetchUserProfile(id: userId) { result in
switch result {
case .success(let profile):
// Assert
XCTAssertEqual(profile.name, "Test User")
XCTAssertEqual(profile.email, "test@example.com")
expectation.fulfill()
case .failure(let error):
XCTFail("Failed with error: \(error)")
}
}
wait(for: [expectation], timeout: 5.0)
} |
|
Разница очевидна — асинхронные тесты более выразительны и требуют меньше кода.
Работа с таймаутами в асинхронных операциях
В реальных приложениях часто требуется ограничить время ожидания асинхронных операций. Создадим функцию, которая добавляет таймаут к любой асинхронной операции:
Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
| struct TimeoutError: Error {
let seconds: TimeInterval
}
extension Task where Success == Never, Failure == Never {
static func timeout<T>(seconds: TimeInterval) async throws -> T where T : Sendable {
try await withThrowingTaskGroup(of: T.self) { group in
group.addTask {
try await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000))
throw TimeoutError(seconds: seconds)
}
return try await group.next()!
}
}
}
// Использование таймаута
func fetchWithTimeout<T>(seconds: TimeInterval, operation: @escaping () async throws -> T) async throws -> T {
return try await withThrowingTaskGroup(of: T.self) { group in
// Добавляем основную операцию
group.addTask {
try await operation()
}
// Добавляем задачу таймаута
group.addTask {
try await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000))
throw TimeoutError(seconds: seconds)
}
// Берем результат первой завершившейся задачи
do {
let result = try await group.next()!
// Отменяем все остальные задачи
group.cancelAll()
return result
} catch {
// Отменяем все задачи при ошибке
group.cancelAll()
throw error
}
}
} |
|
Использовать эту функцию можно так:
Swift | 1
2
3
4
5
6
7
8
9
10
| do {
let data = try await fetchWithTimeout(seconds: 5.0) {
try await networkService.fetchLargeData()
}
processData(data)
} catch let error as TimeoutError {
print("Операция превысила таймаут \(error.seconds) секунд")
} catch {
print("Произошла другая ошибка: \(error)")
} |
|
Взаимодействие async/await с UIKit/SwiftUI
При использовании async/await в iOS-приложениях необходимо понимать, как асинхронный код взаимодействует с UI-фреймворками. Большинство UI-операций должны выполняться на главном потоке, поэтому Swift предоставляет атрибут @MainActor для обозначения кода, который должен выполняться исключительно в главном потоке. В UIKit типичный пример использования async/await выглядит так:
Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
| class ProfileViewController: UIViewController {
private let userID: String
private let userService = UserService()
@IBOutlet weak var nameLabel: UILabel!
@IBOutlet weak var avatarImageView: UIImageView!
init(userID: String) {
self.userID = userID
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
loadUserProfile()
}
private func loadUserProfile() {
Task {
do {
let user = try await userService.fetchUser(id: userID)
// Обновление UI должно происходить на главном потоке
await MainActor.run {
nameLabel.text = user.name
title = user.username
}
// Загружаем аватар
if let avatarURL = user.avatarURL {
let avatarImage = try await userService.fetchImage(from: avatarURL)
await MainActor.run {
avatarImageView.image = avatarImage
}
}
} catch {
await MainActor.run {
showErrorAlert(error)
}
}
}
}
private func showErrorAlert(_ error: Error) {
let alert = UIAlertController(
title: "Ошибка",
message: "Не удалось загрузить профиль: \(error.localizedDescription)",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
} |
|
В SwiftUI интеграция async/await еще проще благодаря нативной поддержке в виде модификатора .task :
Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
| struct ProfileView: View {
let userID: String
@State private var user: User?
@State private var avatarImage: UIImage?
@State private var errorMessage: String?
private let userService = UserService()
var body: some View {
VStack {
if let user = user {
Text(user.name)
.font(.title)
if let avatarImage = avatarImage {
Image(uiImage: avatarImage)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 100, height: 100)
.clipShape(Circle())
} else {
ProgressView()
.frame(width: 100, height: 100)
}
// Другие элементы UI
} else if let errorMessage = errorMessage {
Text("Ошибка: \(errorMessage)")
.foregroundColor(.red)
} else {
ProgressView("Загрузка профиля...")
}
}
.task {
do {
// Загружаем данные пользователя
let loadedUser = try await userService.fetchUser(id: userID)
self.user = loadedUser
// Загружаем аватар, если он есть
if let avatarURL = loadedUser.avatarURL {
self.avatarImage = try await userService.fetchImage(from: avatarURL)
}
} catch {
self.errorMessage = error.localizedDescription
}
}
}
} |
|
Модификатор .task автоматически создает асинхронную задачу, привязанную к жизненному циклу представления — задача запускается при появлении представления и автоматически отменяется, когда представление исчезает.
Оптимизация производительности
Async/await не только делает код более читаемым, но и предоставляет возможности для оптимизации производительности. Рассмотрим несколько техник:
1. Параллельное выполнение независимых операций:
Swift | 1
2
3
4
5
6
7
8
9
10
11
12
| func loadDashboardData() async throws -> DashboardData {
async let userData = userService.fetchUserData()
async let newsItems = newsService.fetchLatestNews()
async let notifications = notificationService.fetchPendingNotifications()
// Все три операции запускаются одновременно!
return try await DashboardData(
user: userData,
news: newsItems,
notifications: notifications
)
} |
|
2. Кооперативная отмена операций: задачи в Swift поддерживают проверку отмены, что позволяет быстро реагировать на измененные условия:
Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
| func processLargeDataset() async throws -> [ProcessedItem] {
var results = [ProcessedItem]()
for item in dataset {
// Периодически проверяем, не была ли задача отменена
try Task.checkCancellation()
let processedItem = try await processItem(item)
results.append(processedItem)
}
return results
} |
|
3. Приоритеты задач: можно устанавливать разные приоритеты для задач, чтобы более важные операции выполнялись раньше:
Swift | 1
2
3
4
5
6
7
8
9
10
11
| // Высокий приоритет для критически важной задачи
Task(priority: .high) {
let criticalData = try await fetchCriticalData()
processCriticalData(criticalData)
}
// Низкий приоритет для фоновой задачи
Task(priority: .low) {
let backgroundData = try await fetchNonCriticalData()
processBackgroundData(backgroundData)
} |
|
4. Отложенная загрузка с помощью AsyncStream: для эффективной обработки потоков данных:
Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| func streamProcessedData() -> AsyncStream<ProcessedItem> {
return AsyncStream { continuation in
Task {
for item in largeDataset {
let processed = await processItem(item)
continuation.yield(processed)
// Проверяем, не отменилась ли задача
if Task.isCancelled {
break
}
}
continuation.finish()
}
}
}
// Использование
Task {
for await item in streamProcessedData() {
await MainActor.run {
updateUI(with: item)
}
}
print("Обработка завершена")
} |
|
Реализация собственного расширения URLSession
Расширение URLSession для работы с асинхронными запросами и автоматической декодификацией ответов:
Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
| extension URLSession {
func data(for request: URLRequest) async throws -> (Data, URLResponse) {
return try await withCheckedThrowingContinuation { continuation in
let task = self.dataTask(with: request) { data, response, error in
if let error = error {
continuation.resume(throwing: error)
return
}
guard let data = data, let response = response else {
continuation.resume(throwing: URLError(.badServerResponse))
return
}
continuation.resume(returning: (data, response))
}
task.resume()
}
}
func decodable<T: Decodable>(for request: URLRequest, decoder: JSONDecoder = JSONDecoder()) async throws -> T {
let (data, response) = try await data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
throw URLError(.badServerResponse)
}
return try decoder.decode(T.self, from: data)
}
}
// Использование
func fetchUsers() async throws -> [User] {
var request = URLRequest(url: URL(string: "https://api.example.com/users")!)
request.httpMethod = "GET"
request.addValue("application/json", forHTTPHeaderField: "Accept")
return try await URLSession.shared.decodable(for: request)
} |
|
Работа с локальной базой данных в асинхронном контексте
Часто в мобильных приложениях требуется взаимодействие не только с сетью, но и с локальной базой данных. Рассмотрим пример реализации асинхронного доступа к Core Data:
Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
| actor CoreDataManager {
static let shared = CoreDataManager()
private let container: NSPersistentContainer
private init() {
container = NSPersistentContainer(name: "MyAppModel")
container.loadPersistentStores { _, error in
if let error = error {
fatalError("Failed to load Core Data stack: \(error)")
}
}
}
func saveUser(_ user: User) async throws {
let context = container.newBackgroundContext()
return try await context.perform {
let userEntity = UserEntity(context: context)
userEntity.id = user.id
userEntity.name = user.name
userEntity.email = user.email
try context.save()
}
}
func fetchUsers() async throws -> [User] {
let context = container.newBackgroundContext()
return try await context.perform {
let fetchRequest = NSFetchRequest<UserEntity>(entityName: "UserEntity")
let userEntities = try context.fetch(fetchRequest)
return userEntities.map { entity in
User(
id: entity.id ?? "",
name: entity.name ?? "",
email: entity.email ?? ""
)
}
}
}
}
// Использование
func syncUserData() async {
do {
// Загружаем пользователей с сервера
let users = try await networkService.fetchUsers()
// Сохраняем в локальную базу данных
for user in users {
try await CoreDataManager.shared.saveUser(user)
}
// Загружаем пользователей из базы данных
let localUsers = try await CoreDataManager.shared.fetchUsers()
print("Локально сохранено \(localUsers.count) пользователей")
} catch {
print("Ошибка синхронизации: \(error)")
}
} |
|
Обратите внимание на использование класса actor для CoreDataManager . Акторы обеспечивают потокобезопасный доступ к своему внутреннему состоянию, что идеально подходит для менеджеров баз данных.
Продвинутые техники
Освоив основы async/await, пора погрузиться в более сложные концепции, позволяющие раскрыть весь потенциал Swift Concurrency.
Группы задач и приоритеты
TaskGroup - мощнейший инструмент для организации динамического числа параллельных операций. В отличие от async let , который требует знать заранее количество асинхронных вызовов, TaskGroup позволяет добавлять задачи динамически:
Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| func loadImagesForProfiles(users: [User]) async throws -> [User: UIImage] {
var userImages = [User: UIImage]()
try await withThrowingTaskGroup(of: (User, UIImage).self) { group in
for user in users {
group.addTask {
if let imageUrl = user.profileImageUrl {
let image = try await self.imageLoader.loadImage(from: imageUrl)
return (user, image)
} else {
return (user, UIImage(named: "default_avatar")!)
}
}
}
// Собираем результаты
for try await (user, image) in group {
userImages[user] = image
}
}
return userImages
} |
|
Важное преимущество TaskGroup в том, что он автоматически ограничивает количество параллельно выполняющихся задач, предотвращая перегрузку системы. Кроме того, при выходе из блока группы все оставшиеся задачи автоматически отменяются.
Приоритеты задач позволяют контролировать порядок выполнения конкурирующих операций:
Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| Task(priority: .high) {
// Критичная операция
}
Task(priority: .medium) {
// Операция средней важности
}
Task(priority: .low) {
// Фоновая операция
}
Task(priority: .background) {
// Наименее приоритетная операция
} |
|
Приоритеты наследуются дочерними задачами, но их можно переопределить при создании новых. Это особенно удобно при разработке сложных многоуровневых асинхронных потоков.
Отмена асинхронных операций
Отмена операций - критически важный аспект производительных мобильных приложений. В Swift Concurrency отмена интегрирована на уровне системы и распространяется по цепочке задач. Есть два основных способа отменить задачу:
Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // 1. Явная отмена
let task = Task {
try await performLongOperation()
}
// Позже
task.cancel()
// 2. Автоматическая отмена при выходе из области видимости
if needToFetchData {
withTaskGroup(of: Void.self) { group in
group.addTask { await fetchSomeData() }
group.addTask { await fetchMoreData() }
// Все задачи в группе автоматически отменяются при выходе из блока
}
} |
|
Для корректной работы с отменой внутри асинхронного кода нужно периодически проверять состояние отмены:
Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
| func processLargeDataSet() async throws -> [Result] {
var results = [Result]()
for item in dataSet {
// Проверяем отмену перед каждой итерацией
try Task.checkCancellation()
let processedItem = try await process(item)
results.append(processedItem)
}
return results
} |
|
Или более элегантный вариант с использованием свойства Task.isCancelled :
Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| func processLargeDataSet() async throws -> [Result] {
var results = [Result]()
for item in dataSet where !Task.isCancelled {
let processedItem = try await process(item)
results.append(processedItem)
}
// Если задача была отменена, генерируем ошибку
if Task.isCancelled {
throw CancellationError()
}
return results
} |
|
Интересный факт: отмена в Swift не происходит моментально и насильно. Вместо этого, задача помечается как "запрошенная к отмене", а код внутри задачи должен проверять этот статус и корректно прекратить свою работу. Это предотвращает проблемы с ресурсами, находящимися в несогласованном состоянии.
AsyncSequence и его возможности
AsyncSequence — одно из самых недооцененных сокровищ Swift Concurrency. Этот протокол позволяет работать с асинхронными потоками данных так же, как с обычными последовательностями в Swift, но без блокировки потоков:
Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| struct NumberGenerator: AsyncSequence {
typealias Element = Int
let upperBound: Int
struct AsyncIterator: AsyncIteratorProtocol {
var current = 1
let upperBound: Int
mutating func next() async -> Int? {
guard current <= upperBound else { return nil }
let value = current
current += 1
// Имитация асинхронной работы
await Task.sleep(for: .milliseconds(100))
return value
}
}
func makeAsyncIterator() -> AsyncIterator {
AsyncIterator(upperBound: upperBound)
}
}
// Использование
func printNumbers() async {
let numbers = NumberGenerator(upperBound: 10)
for await number in numbers {
print("Получено число: \(number)")
// Числа будут выводиться с интервалом в 100 мс
}
} |
|
AsyncSequence особенно полезен при работе с потоками событий или частями больших данных. Например, вместо ожидания загрузки всего файла, можно обрабатывать его построчно:
Swift | 1
2
3
4
5
6
| let fileHandle = try FileHandle(forReadingFrom: fileURL)
let lines = fileHandle.bytes.lines
for try await line in lines {
process(line)
} |
|
Или при работе с WebSocket-соединениями:
Swift | 1
2
3
4
5
6
7
8
9
10
11
12
| let webSocketStream = URLSession.shared.webSocketTask(with: url).messages
for try await message in webSocketStream {
switch message {
case .data(let data):
processData(data)
case .string(let text):
processText(text)
@unknown default:
break
}
} |
|
Взаимодействие между async/await и Combine
Если вы уже используете Combine в своих проектах, вам пригодятся мосты между Combine и Swift Concurrency:
Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| // Из Publisher в AsyncSequence
extension Publisher {
var values: AsyncThrowingStream<Output, Error> {
AsyncThrowingStream { continuation in
let cancellable = self.sink(
receiveCompletion: { completion in
switch completion {
case .finished:
continuation.finish()
case .failure(let error):
continuation.finish(throwing: error)
}
},
receiveValue: { value in
continuation.yield(value)
}
)
continuation.onTermination = { _ in
cancellable.cancel()
}
}
}
}
// Использование
let publisher = URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.decode(type: [User].self, decoder: JSONDecoder())
Task {
do {
for try await users in publisher.values {
updateUI(with: users)
}
} catch {
handleError(error)
}
} |
|
Также можно создавать Combine-издатели из асинхронных функций:
Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| func createPublisher<T>(from asyncFunction: @escaping () async throws -> T) -> AnyPublisher<T, Error> {
Future { promise in
Task {
do {
let result = try await asyncFunction()
promise(.success(result))
} catch {
promise(.failure(error))
}
}
}.eraseToAnyPublisher()
}
// Использование
let usersPublisher = createPublisher { try await userService.fetchUsers() }
usersPublisher
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { completion in
if case .failure(let error) = completion {
print("Error: \(error)")
}
},
receiveValue: { users in
print("Received \(users.count) users")
}
)
.store(in: &cancellables) |
|
Структурированная конкурентность и избежание гонок данных
Главная цель структурированной конкурентности — создать код, который безопасен по отношению к потокам и легко поддается анализу. Swift использует атрибут @Sendable и специальный тип actor для обеспечения безопасности.
Атрибут @Sendable обозначает замыкания, которые могут быть безопасно переданы между потоками:
Swift | 1
2
3
4
5
| func performAsync(@Sendable _ operation: @escaping () -> Void) {
Task {
operation()
}
} |
|
Компилятор проверяет, что замыкания, помеченные как @Sendable , не захватывают мутабельных ссылок, которые могли бы привести к гонкам данных.
Aкторы — новый тип в Swift, специально созданный для безопасного доступа к мутабельному состоянию из конкурентного кода:
Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| actor MessageStore {
private var messages: [Message] = []
func add(_ message: Message) {
messages.append(message)
}
func message(at index: Int) -> Message? {
guard index < messages.count else { return nil }
return messages[index]
}
func sendAll() async {
for message in messages {
await sendMessage(message)
}
}
private func sendMessage(_ message: Message) async {
// Отправка сообщения
}
}
// Использование
let store = MessageStore()
// При обращении к методам актора из внешнего кода необходимо использовать await
Task {
await store.add(Message(text: "Hello"))
if let message = await store.message(at: 0) {
print(message.text)
}
} |
|
Доступ к состоянию актора синхронизирован таким образом, что в любой момент только один поток может модифицировать его данные, что предотвращает гонки данных.
Профилирование и отладка асинхронного кода
Отладка асинхронного кода всегда была нетривиальной задачей, а с появлением async/await стала еще интереснее. Swift и Xcode предлагают несколько инструментов, которые помогут вам выявить проблемы в конкурентном коде:
Инструмент Thread Sanitizer
Thread Sanitizer (TSan) — отличный помощник для выявления проблем с гонками данных. Включить его можно в настройках схемы запуска проекта, в разделе Diagnostics:
Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| // Пример кода, который вызовет предупреждение Thread Sanitizer
class UnsafeCounter {
var count = 0
func increment() {
count += 1
}
}
let counter = UnsafeCounter()
// Параллельный доступ к мутабельному состоянию без синхронизации
Task {
for _ in 0..<1000 {
counter.increment()
}
}
Task {
for _ in 0..<1000 {
counter.increment()
}
} |
|
TSan отловит эту ситуацию и укажет строки, где происходит небезопасный доступ к данным.
Символы приостановки в отладчике
При отладке асинхронного кода в Xcode вы заметите специальные символы приостановки (suspension points) в трассировке стека. Эти значки показывают, где функция была приостановлена и где она будет возобновлена после выполнения асинхронной операции.
Паттерны проектирования для асинхронного программирования
Async/await не только делает код чище, но и позволяет применять знакомые паттерны проектирования в новом контексте:
Паттерн "Репозиторий" с async/await
Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| protocol UserRepository {
func getUser(id: String) async throws -> User
func saveUser(_ user: User) async throws
func deleteUser(id: String) async throws
}
class APIUserRepository: UserRepository {
func getUser(id: String) async throws -> User {
// Реализация через API
}
func saveUser(_ user: User) async throws {
// Реализация через API
}
func deleteUser(id: String) async throws {
// Реализация через API
}
}
class LocalUserRepository: UserRepository {
func getUser(id: String) async throws -> User {
// Реализация через локальную базу данных
}
func saveUser(_ user: User) async throws {
// Реализация через локальную базу данных
}
func deleteUser(id: String) async throws {
// Реализация через локальную базу данных
}
} |
|
Паттерн "Декоратор" для асинхронных операций
Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| protocol CacheRepository {
func getData(for key: String) async throws -> Data
}
class NetworkRepository: CacheRepository {
func getData(for key: String) async throws -> Data {
// Загрузка из сети
}
}
class CachingDecorator: CacheRepository {
private let wrappedRepository: CacheRepository
private var cache: [String: Data] = [:]
init(_ repository: CacheRepository) {
self.wrappedRepository = repository
}
func getData(for key: String) async throws -> Data {
if let cachedData = cache[key] {
return cachedData
}
let data = try await wrappedRepository.getData(for key)
cache[key] = data
return data
}
}
// Использование
let repository = CachingDecorator(NetworkRepository())
let data = try await repository.getData(for: "user_123") |
|
Расширение стандартных типов Swift для поддержки async/await
Стандартные библиотеки Swift можно расширять для добавления асинхронной функциональности:
Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
| extension Array where Element: Sendable {
func asyncMap<T: Sendable>(_ transform: @Sendable (Element) async throws -> T) async rethrows -> [T] {
var results = [T]()
for element in self {
let transformed = try await transform(element)
results.append(transformed)
}
return results
}
func asyncParallelMap<T: Sendable>(_ transform: @Sendable (Element) async throws -> T) async throws -> [T] {
try await withThrowingTaskGroup(of: (Int, T).self) { group in
for (index, element) in self.enumerated() {
group.addTask {
let result = try await transform(element)
return (index, result)
}
}
var results = [T?](repeating: nil, count: self.count)
for try await (index, result) in group {
results[index] = result
}
return results.compactMap { $0 }
}
}
}
extension Collection {
func asyncForEach(_ operation: (Element) async throws -> Void) async rethrows {
for element in self {
try await operation(element)
}
}
func asyncParallelForEach(_ operation: @Sendable (Element) async throws -> Void) async throws where Element: Sendable {
try await withThrowingTaskGroup(of: Void.self) { group in
for element in self {
group.addTask {
try await operation(element)
}
}
// Ждем завершения всех задач
try await group.waitForAll()
}
}
}
// Примеры использования
let userIds = ["user1", "user2", "user3"]
// Последовательное выполнение
let users = await userIds.asyncMap { id in
await fetchUser(id: id)
}
// Параллельное выполнение
let parallelUsers = try await userIds.asyncParallelMap { id in
try await fetchUser(id: id)
}
// Обработка коллекции без возврата результата
await ["image1.jpg", "image2.jpg", "image3.jpg"].asyncForEach { filename in
await saveImage(filename)
}
// Параллельная обработка без возврата результата
try await ["image1.jpg", "image2.jpg", "image3.jpg"].asyncParallelForEach { filename in
try await processImage(filename)
} |
|
Такие расширения делают работу с коллекциями в асинхронном контексте более естественной и выразительной.
В целом, продвинутые техники Swift Concurrency открывают новые возможности для создания безопасного и поддерживаемого асинхронного кода. Сочетание понятного синтаксиса и абстракций делает async/await незаменимым инструментом в арсенале современного Swift-разработчика.
Ios 8.x в iphone 4s или оставить ios 7.x? стоит ли перепрошивать 4s или остаться на семерке.... думаю что 8ка будет работать медленне? Асинхронное программирование, Async и Await нужно сделать программу, которая в отдельном методе заполняет массив случайными числами. Пока массив заполняется, вывести в консоль какой-то текст, а... Асинхронное программирование await async Всем привет!
Пытаюсь разобраться с асинхронным вызовом методов. Узнал такую вещь: если предполагается, что некоторый метод может долго... Асинхронное программирование, await, async Здравствуйте, нужна помощь с этим методом. Мне надо создать бота для Telegram что бы он отправлял мне сообщения но проблема не в этом. В коде когда... MutationObserver не перехватывает программные события Подскажите пожалуйста, вот ставлю MutationObserver на элемент к примеру ввода. Затем просто веду курсор мышки на элемент ввода и MutationObserver -... Не получается изменить имя родительского блока в цикле массива Есть функция, которая печатает имя пользователя и его числа.
При выводе результата в echo(я эти две строки пометил комментами)
я создаю... Async/ await как правильно ввести данные в async метод (консоль) Привет , кто то может помочь ?)
проблема в тому что у меня есть async метод который запускается из Main, по среди этого метода вызывается еще один... Async/await Мне нужно, чтобы пароль выводился по одному символу
public Matrix()
{
InitializeComponent();
this.WindowState... Async, await Объясните работу async, await. Облазил весь интернет, но ничего толкового не нашел. Заранее спасибо)) async/await https://metanit.com/sharp/tutorial/13.7.php
вот код Task<T>:
// определение асинхронного метода
static async... Async await Пытыюсь разобраться с async/await но что то без успешно пока. Не подскажете как переделать этот код на примере проще освоить
public partial... Async/await В интернете копался ничего информативного не нашел, все в каких- то не понятных для новичка терминах, объясните пожалуйста смысл асинхронного...
|