Введение в конкурентность и многопоточность в 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 существует несколько путей реализации многопоточности, и выбор зависит от конкретной задачи:
- GCD