Конкурентность и многопоточность в Swift: продвинутый уровень

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

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

Начать курс

Конкурентность и многопоточность в Swift: продвинутый уровень

Современные приложения для iOS и macOS требуют высокой производительности и отзывчивости интерфейса. Продвинутая работа с конкурентностью в Swift выходит далеко за рамки простого использования Grand Central Dispatch (GCD). В этой статье мы рассмотрим ключевые инструменты Swift 5.5+ для управления асинхронными задачами: async/await, Actors, TaskGroup и протокол Sendable. Вы узнаете, как избежать гонок данных, правильно структурировать многопоточный код и оптимизировать производительность.

1. Основы async/await и Structured Concurrency

Ключевые слова async и await — это основа современной конкурентности в Swift. Они позволяют писать асинхронный код, который выглядит как синхронный, что значительно упрощает чтение и отладку.

Объявление асинхронной функции:

func fetchUserData() async throws -> User {
    let url = URL(string: "https://api.example.com/user")!
    let (data, _) = try await URLSession.shared.data(from: url)
    let decoder = JSONDecoder()
    return try decoder.decode(User.self, from: data)
}

Вызов асинхронной функции с обработкой ошибок:

Task {
    do {
        let user = try await fetchUserData()
        print("User name: \(user.name)")
    } catch {
        print("Failed to fetch user: \(error)")
    }
}

Важные моменты:

  • Structured Concurrency: Каждая асинхронная задача имеет четкое время жизни, ограниченное областью видимости родительского Task.
  • Отмена задач: Автоматическая отмена дочерних задач при отмене родительского Task.
  • Приоритеты: Используйте Task(priority: .userInitiated) для задач, важных для пользователя.

2. Actors: защита от гонок данных

До Swift 5.5 разработчики часто использовали сериализованные очереди GCD для защиты общего состояния. Actors предоставляют встроенную изоляцию: доступ к состоянию actor'а возможен только через await, что гарантирует потокобезопасность на уровне компилятора.

Пример Actor для банковского счета:

actor BankAccount {
    private var balance: Double = 0.0

    func deposit(amount: Double) {
        balance += amount
    }

    func withdraw(amount: Double) throws {
        guard balance >= amount else {
            throw BankError.insufficientFunds
        }
        balance -= amount
    }

    func getBalance() -> Double {
        return balance
    }
}

// Использование
let account = BankAccount()
Task {
    await account.deposit(amount: 100.0)
    let currentBalance = await account.getBalance()
    print("Balance: \(currentBalance)")
}

Преимущества Actors:

  • Компиляторная проверка: Попытка доступа к свойству actor'а без await вызовет ошибку компиляции.
  • Повторный вход (Reentrancy): Actor может приостановить выполнение текущего метода (например, при await внутри actor'а) и обработать другой вызов, что предотвращает deadlocks.
  • Производительность: Actors используют оптимизированные механизмы синхронизации, часто быстрее ручных блокировок.

3. TaskGroup: параллельная обработка данных

Когда нужно выполнить несколько независимых асинхронных операций и дождаться их завершения, TaskGroup — идеальный инструмент. Он автоматически управляет количеством параллельных задач и собирает результаты.

Пример загрузки нескольких изображений параллельно:

func downloadImages(urls: [URL]) async throws -> [UIImage] {
    return try await withThrowingTaskGroup(of: UIImage.self) { group in
        for url in urls {
            group.addTask {
                let (data, _) = try await URLSession.shared.data(from: url)
                guard let image = UIImage(data: data) else {
                    throw ImageError.invalidData
                }
                return image
            }
        }

        var images: [UIImage] = []
        for try await image in group {
            images.append(image)
        }
        return images
    }
}

Ключевые особенности TaskGroup:

  • Ограничение параллелизма: По умолчанию Swift использует оптимальное количество потоков, но вы можете добавить логику для ограничения (например, через семафоры).
  • Обработка ошибок: Используйте withThrowingTaskGroup для проброса ошибок; если одна задача выбросит ошибку, все остальные задачи будут автоматически отменены.
  • Сбор результатов в порядке завершения: for try await обрабатывает результаты по мере их готовности, а не в порядке добавления.

4. Sendable и MainActor: безопасное взаимодействие с UI

Протокол Sendable гарантирует, что данные можно безопасно передавать между изолированными доменами (например, между Actor и Task). @MainActor — это специальный глобальный actor, который гарантирует выполнение кода на главном потоке.

Пример использования @MainActor и Sendable:

// Структура, которую можно безопасно передавать между потоками
struct UserData: Sendable {
    let id: Int
    let name: String
}

class ViewModel: ObservableObject {
    @Published var users: [UserData] = []

    @MainActor
    func updateUI(with newUsers: [UserData]) {
        // Этот метод гарантированно выполняется на главном потоке
        self.users = newUsers
    }

    func loadUsers() async {
        let fetchedUsers = await fetchUsersFromAPI()
        // Вызов @MainActor функции из фонового контекста
        await updateUI(with: fetchedUsers)
    }
}

// Функция, возвращающая Sendable данные
func fetchUsersFromAPI() async -> [UserData] {
    // ... асинхронный запрос
    return [UserData(id: 1, name: "Alice")]
}

Рекомендации: