Асинхронное программирование в Swift: полное руководство на практике

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Асинхронное программирование в 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: "                

Блоги

Рекомендаций по книгам