Введение в конкурентность и многопоточность в Swift
Современные мобильные и десктопные приложения должны быть быстрыми и отзывчивыми. В разработке под iOS и macOS ключевую роль играет правильное управление конкурентностью и многопоточностью. Swift предоставляет мощные инструменты для параллельного выполнения задач, начиная от классического Grand Central Dispatch (GCD) и заканчивая современной моделью async/await и акторами.
В этой статье мы разберем все основные подходы к работе с потоками в Swift: от базовых концепций до продвинутых техник. Вы узнаете, как избежать race conditions, deadlocks и других проблем, характерных для многопоточного программирования.
Grand Central Dispatch (GCD) — классика многопоточности
Grand Central Dispatch (GCD) — это низкоуровневая библиотека Apple для управления параллельным выполнением кода. Она использует очереди (dispatch queues) для распределения задач по потокам.
Основные типы очередей
В GCD существует два основных типа очередей:
- Serial queues (последовательные): задачи выполняются одна за другой в порядке добавления. Идеально подходят для синхронизации доступа к ресурсам.
- Concurrent queues (параллельные): задачи могут выполняться одновременно, порядок завершения не гарантируется.
Пример работы с GCD
import Foundation
// Создаем последовательную очередь
let serialQueue = DispatchQueue(label: "com.example.serial")
// Создаем параллельную очередь
let concurrentQueue = DispatchQueue(label: "com.example.concurrent", attributes: .concurrent)
// Асинхронное выполнение
serialQueue.async {
print("Задача 1 выполняется в фоне")
}
// Синхронное выполнение (блокирует текущий поток)
serialQueue.sync {
print("Задача 2 выполняется синхронно")
}
// Работа с главным потоком
DispatchQueue.main.async {
print("Обновление UI на главном потоке")
}
// Задержка выполнения
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
print("Выполнено через 2 секунды")
}
Dispatch Group — синхронизация группы задач
Dispatch Group позволяет отслеживать завершение группы асинхронных задач и выполнить код после их окончания.
let group = DispatchGroup()
let queue = DispatchQueue.global(qos: .background)
for i in 1...5 {
queue.async(group: group) {
print("Задача \(i) началась")
Thread.sleep(forTimeInterval: Double.random(in: 0.5...2.0))
print("Задача \(i) завершена")
}
}
group.notify(queue: DispatchQueue.main) {
print("Все задачи выполнены! Обновляем UI.")
}
Современный async/await в Swift 5.5+
Начиная с Swift 5.5, Apple представила новую модель конкурентности, основанную на ключевых словах async и await. Это делает асинхронный код более читаемым и понятным.
Определение асинхронных функций
Асинхронные функции помечаются ключевым словом async, а их вызов требует await:
import Foundation
// Асинхронная функция загрузки данных
func fetchUserData() async throws -> String {
let url = URL(string: "https://api.example.com/user")!
let (data, _) = try await URLSession.shared.data(from: url)
return String(data: data, encoding: .utf8) ?? "No data"
}
// Использование async/await
func loadUserProfile() async {
do {
let userData = try await fetchUserData()
await MainActor.run {
print("Данные пользователя: \(userData)")
}
} catch {
print("Ошибка загрузки: \(error)")
}
}
// Запуск асинхронной задачи
Task {
await loadUserProfile()
}
Async let — параллельное выполнение
Ключевое слово async let позволяет запускать несколько асинхронных задач параллельно:
func loadMultipleResources() async throws -> (String, String) {
async let resource1 = fetchUserData()
async let resource2 = fetchResource2()
// Ожидаем завершения обеих задач
return try await (resource1, resource2)
}
Task {
let (data1, data2) = try await loadMultipleResources()
print("Оба ресурса загружены: \(data1), \(data2)")
}
Акторы (Actors) — безопасная работа с общими данными
Акторы — это новый механизм в Swift, который защищает данные от race conditions. Актор гарантирует, что доступ к его состоянию всегда происходит в безопасном контексте.
Создание актора
actor BankAccount {
private var balance: Double = 0.0
func deposit(amount: Double) {
balance += amount
print("Депозит: \(amount). Баланс: \(balance)")
}
func withdraw(amount: Double) -> Bool {
if balance >= amount {
balance -= amount
print("Снятие: \(amount). Баланс: \(balance)")
return true
}
return false
}
func getBalance() -> Double {
return balance
}
}
// Использование актора
let account = BankAccount()
Task {
await account.deposit(amount: 1000)
let success = await account.withdraw(amount: 500)
print("Снятие успешно: \(success)")
let currentBalance = await account.getBalance()
print("Текущий баланс: \(currentBalance)")
}
Преимущества акторов
- Автоматическая синхронизация доступа к свойствам и методам
- Предотвращение гонок данных на уровне компилятора
- Упрощение кода по сравнению с ручной синхронизацией через GCD
- Интеграция с async/await для удобного асинхронного вызова
OperationQueue — высокоуровневый API для управления задачами
OperationQueue предоставляет более высокий уровень абстракции по сравнению с GCD. Вы работаете с объектами Operation, которые можно настраивать, отменять и связывать между собой.
Базовый пример с OperationQueue
import Foundation
// Создаем очередь операций
let operationQueue = OperationQueue()
operationQueue.maxConcurrentOperationCount = 2 // Ограничиваем до 2 параллельных операций
// Создаем операции
let operation1 = BlockOperation {
print("Операция 1 началась")
Thread.sleep(forTimeInterval: 1.0)
print("Операция 1 завершена")
}
let operation2 = BlockOperation {
print("Операция 2 началась")
Thread.sleep(forTimeInterval: 1.5)
print("Операция 2 завершена")
}
// Устанавливаем зависимость: operation2 выполнится только после operation1
operation2.addDependency(operation1)
// Добавля