Асинхронное программирование в Swift: полное руководство на практике
Асинхронное программирование — одна из ключевых концепций современной разработки под iOS и macOS. В Swift, начиная с версии 5.5, появилась мощная система Structured Concurrency, которая сделала работу с асинхронностью простой и безопасной. В этой статье мы разберём основные инструменты: async/await, Task, Actor и Sendable, а также покажем их применение на реальных примерах.
1. Основы async/await: как перестать писать замыкания и начать жить
До появления async/await асинхронный код в Swift писался с помощью замыканий и делегатов, что часто приводило к «аду колбэков». Современный подход позволяет писать асинхронный код так же линейно, как синхронный.
Объявить асинхронную функцию можно с помощью ключевого слова async. Вызов такой функции требует контекста, поддерживающего асинхронность, и ключевого слова await.
// Пример асинхронной функции загрузки данных
func fetchUserData(userID: String) async throws -> User {
let url = URL(string: "https://api.example.com/users/\(userID)")!
let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw URLError(.badServerResponse)
}
let user = try JSONDecoder().decode(User.self, from: data)
return user
}
// Использование
Task {
do {
let user = try await fetchUserData(userID: "123")
print("User name: \(user.name)")
} catch {
print("Error: \(error)")
}
}
Ключевые особенности:
async— маркер асинхронной функцииawait— точка приостановки выполнения- Функция может быть как
async, так иthrowsодновременно - Поток не блокируется — во время ожидания он может выполнять другую работу
2. Task и TaskGroup: управление параллельными задачами
Task — это основная единица асинхронной работы. Она создаёт новую асинхронную задачу, которая выполняется в глобальном пуле потоков. TaskGroup позволяет запускать несколько задач параллельно и собирать их результаты.
Создание и отмена Task
// Создание задачи
let task = Task {
let result = await performLongOperation()
return result
}
// Получение результата
let value = await task.value
// Отмена задачи
task.cancel()
// Проверка отмены внутри задачи
func performLongOperation() async -> String {
// Проверяем, не отменена ли задача
guard !Task.isCancelled else { return "Cancelled" }
// Эмулируем долгую работу
try? await Task.sleep(nanoseconds: 2_000_000_000)
return "Completed"
}
Параллельная обработка с TaskGroup
// Загрузка нескольких изображений параллельно
func loadImages(urls: [URL]) async throws -> [UIImage] {
return try await withThrowingTaskGroup(of: UIImage.self) { group in
var images: [UIImage] = []
images.reserveCapacity(urls.count)
for url in urls {
group.addTask {
let (data, _) = try await URLSession.shared.data(from: url)
guard let image = UIImage(data: data) else {
throw URLError(.cannotDecodeContentData)
}
return image
}
}
// Собираем результаты по мере их готовности
for try await image in group {
images.append(image)
}
return images
}
}
Важно: TaskGroup автоматически управляет жизненным циклом дочерних задач. Если одна задача выбрасывает ошибку, остальные задачи могут быть отменены (зависит от реализации).
3. Actor: защита разделяемых данных от гонок
Actor — это ссылочный тип, который защищает своё состояние от одновременного доступа из нескольких задач. Все методы и свойства actor выполняются последовательно, что исключает состояние гонки.
// Определение actor
actor BankAccount {
private var balance: Double = 0
func deposit(amount: Double) {
balance += amount
print("Deposited \(amount). New balance: \(balance)")
}
func withdraw(amount: Double) throws {
guard balance >= amount else {
throw BankError.insufficientFunds
}
balance -= amount
print("Withdrew \(amount). New balance: \(balance)")
}
func getBalance() -> Double {
return balance
}
}
// Использование
let account = BankAccount()
Task {
await account.deposit(amount: 100)
try await account.withdraw(amount: 50)
let balance = await account.getBalance()
print("Final balance: \(balance)")
}
// Попытка одновременного доступа — безопасна
Task {
await account.deposit(amount: 200)
}
Task {
try await account.withdraw(amount: 30)
}
Правила работы с Actor:
- Все публичные методы и свойства требуют
awaitпри вызове извне - Внутри actor можно обращаться к свойствам без
await - Нельзя передавать изменяемые ссылочные типы между actor без маркера
Sendable - Actor наследуются от
Actorпротокола
4. Sendable и глобальные переменные: безопасная передача данных
Протокол Sendable гарантирует, что значение можно безопасно передавать между задачами. Все value-типы (структуры, перечисления) автоматически соответствуют Sendable, если их свойства тоже Sendable. Ссылочные типы требуют явного указания.
// Структура автоматически Sendable
struct UserData: Sendable {
let id: String
let name: String
let email: String
}
// Класс с ручным управлением
final class ImageCache: @unchecked Sendable {
private var cache = NSCache<NSString, UIImage>()
func getImage(for key: String) -> UIImage? {
return cache.object(forKey: key as NSString)
}
func setImage(_ image: UIImage, for key: String) {
cache.setObject(image, forKey: key as NSString)
}
}
// Глобальная переменная должна быть Sendable
let globalCache = ImageCache()
// Безопасное использование
Task {
let image = await fetchImage(url: "https://example.com/image.png")
globalCache.setImage(image, for: "