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

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

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

Начать курс

Введение в конкурентность и многопоточность в Swift

Современные iOS и macOS приложения всё чаще требуют выполнения нескольких задач одновременно. Будь то загрузка данных из сети, обработка изображений или сложные математические вычисления — без грамотного управления потоками не обойтись. В Swift конкурентность и многопоточность реализованы на высочайшем уровне, предоставляя разработчику мощные и безопасные инструменты.

В этой статье мы разберём три основных подхода: Grand Central Dispatch (GCD), OperationQueue и современное ключевое слово async/await, появившееся в Swift 5.5. Вы узнаете, как правильно выбирать между ними и избегать типичных ошибок, таких как состояние гонки (race condition) и взаимные блокировки (deadlock).

Grand Central Dispatch (GCD) — классика многопоточности в Swift

Grand Central Dispatch (GCD) — это низкоуровневая библиотека Apple, управляющая пулом потоков. Разработчику не нужно создавать и уничтожать потоки вручную — GCD делает это автоматически, распределяя задачи по доступным ядрам процессора.

Основные очереди GCD

В GCD существует два типа очередей: последовательные (serial) и параллельные (concurrent). Задачи в последовательной очереди выполняются одна за другой, а в параллельной — могут запускаться одновременно.

// Создание последовательной очереди
let serialQueue = DispatchQueue(label: "com.example.serial")
serialQueue.async {
    print("Задача 1")
}
serialQueue.async {
    print("Задача 2")
}

// Создание параллельной очереди
let concurrentQueue = DispatchQueue(label: "com.example.concurrent", attributes: .concurrent)
concurrentQueue.async {
    print("Параллельная задача A")
}
concurrentQueue.async {
    print("Параллельная задача B")
}

Глобальные очереди и QoS

GCD предоставляет готовые глобальные очереди с разными приоритетами (Quality of Service). Используйте их, когда не требуется создавать собственную очередь:

  • .userInteractive — для UI-обновлений (высший приоритет)
  • .userInitiated — для задач, инициированных пользователем
  • .utility — для длительных фоновых операций
  • .background — для задач, не критичных ко времени
DispatchQueue.global(qos: .userInitiated).async {
    // Тяжёлая работа
    let result = performComplexCalculation()
    DispatchQueue.main.async {
        // Возвращаемся на главный поток для обновления UI
        self.updateUI(with: result)
    }
}

Важно: Всегда обновляйте UI только на главном потоке (DispatchQueue.main). Игнорирование этого правила приводит к непредсказуемым сбоям приложения.

OperationQueue — управление зависимостями задач

Если GCD — это «швейцарский нож», то OperationQueue — полноценный планировщик с возможностью отмены, приоритизации и создания зависимостей между операциями. Каждая операция — это экземпляр класса Operation (или его подкласса BlockOperation).

Создание и выполнение операций

let operationQueue = OperationQueue()
operationQueue.maxConcurrentOperationCount = 2 // Ограничиваем количество параллельных операций

let operation1 = BlockOperation {
    print("Операция 1")
}
let operation2 = BlockOperation {
    print("Операция 2")
}

// Добавляем зависимость: operation2 начнётся только после завершения operation1
operation2.addDependency(operation1)

operationQueue.addOperations([operation1, operation2], waitUntilFinished: false)

Отмена и приоритеты

В отличие от GCD, OperationQueue позволяет отменить конкретную операцию или всю очередь:

// Отмена одной операции
operation1.cancel()

// Отмена всей очереди
operationQueue.cancelAllOperations()

// Установка приоритета
operation1.queuePriority = .veryHigh

Используйте OperationQueue, когда нужно тонко управлять порядком выполнения или отменять длительные задачи (например, загрузку файлов).

Swift Concurrency: async/await и Structured Concurrency

С выходом Swift 5.5 Apple представила встроенную модель конкурентности, основанную на ключевых словах async и await. Это революционный шаг, который делает многопоточный код читаемым и безопасным, как синхронный.

Базовый синтаксис async/await

// Асинхронная функция
func fetchUserData() async throws -> User {
    let url = URL(string: "https://api.example.com/user")!
    let (data, _) = try await URLSession.shared.data(from: url)
    return try JSONDecoder().decode(User.self, from: data)
}

// Вызов асинхронной функции
Task {
    do {
        let user = try await fetchUserData()
        print("Пользователь: \(user.name)")
    } catch {
        print("Ошибка загрузки: \(error)")
    }
}

Структурированная конкурентность

Ключевое преимущество новой модели — структурированная конкурентность. Задачи автоматически отменяются при выходе из области видимости, а ошибки обрабатываются стандартными механизмами do-catch:

func loadMultipleUsers() async throws -> [User] {
    // Параллельная загрузка нескольких пользователей
    async let user1 = fetchUserData(id: 1)
    async let user2 = fetchUserData(id: 2)
    async let user3 = fetchUserData(id: 3)
    
    // Ожидание всех результатов
    return try await [user1, user2, user3]
}

Акторы (Actors) — защита от гонки данных

Для защиты общего состояния от одновременного изменения Swift ввёл акторы — классы, которые гарантируют последовательный доступ к своим свойствам:

actor BankAccount {
    private var balance: Double = 0
    
    func deposit(amount: Double) {
        balance += amount
    }
    
    func withdraw(amount: Double) -> Bool {
        if balance >= amount {
            balance -= amount
            return true
        }
        return false
    }
}

// Использование актора
let account = BankAccount()
Task {
    await account.deposit(amount: 100)
    let success = await account.withdraw(amount: 50)
    print(success ? "Успешно" : "Недостаточно средств")
}

Акторы — идеальное решение для работы с общими ресурсами (базы данных, счётчики, кэши). Они полностью исключают состояние гонки на уровне компилятора.

Заключение: что выбрать?

В Swift существует несколько путей реализации многопоточности, и выбор зависит от конкретной задачи: