Конкурентность и многопоточность в Swift: продвинутый уровень
Конкурентность (concurrency) — одна из самых сложных и важных тем в разработке под iOS/macOS. На продвинутом уровне Swift предлагает мощные инструменты: от классического Grand Central Dispatch (GCD) до современной модели async/await и строгой системы Actors. В этой статье мы разберем, как эффективно управлять потоками, избегать гонок данных и писать потокобезопасный код.
1. Grand Central Dispatch (GCD): работа с очередями и группами
GCD остается основой многопоточности в Swift. Однако на продвинутом уровне важно понимать не только разницу между main и global(), но и уметь управлять зависимостями задач.
Создание собственных очередей
let serialQueue = DispatchQueue(label: "com.pythonlib.serial")
let concurrentQueue = DispatchQueue(label: "com.pythonlib.concurrent", attributes: .concurrent)
serialQueue.async {
print("Задача 1 в последовательной очереди")
}
concurrentQueue.async {
print("Задача 2 в параллельной очереди")
}
DispatchGroup: синхронизация нескольких задач
let group = DispatchGroup()
let queue = DispatchQueue.global()
queue.async(group: group) {
sleep(1)
print("Загрузка данных 1")
}
queue.async(group: group) {
sleep(2)
print("Загрузка данных 2")
}
group.notify(queue: DispatchQueue.main) {
print("Все загрузки завершены, обновляем UI")
}
Используйте DispatchGroup, когда нужно дождаться выполнения нескольких параллельных операций (например, загрузка картинок или парсинг JSON).
2. Современная модель async/await и Task
Начиная с Swift 5.5, язык получил нативную поддержку асинхронности через ключевые слова async и await. Это упрощает чтение кода и избавляет от "пирамиды замыканий".
Объявление и вызов асинхронной функции
func fetchUserData() async -> String {
// Имитация сетевого запроса
try? await Task.sleep(nanoseconds: 2_000_000_000)
return "User data loaded"
}
Task {
let result = await fetchUserData()
print(result) // Выведет "User data loaded"
}
Параллельные вызовы с async let
func loadImage() async -> Data { return Data() }
func loadMetadata() async -> String { return "meta" }
Task {
async let image = loadImage()
async let metadata = loadMetadata()
let result = try await (image, metadata)
print("Изображение: \(result.0), метаданные: \(result.1)")
}
async let позволяет запустить несколько асинхронных задач параллельно и дождаться их всех. Это эффективнее последовательного вызова.
3. Actors: потокобезопасность на уровне языка
Акторы (Actors) — это новый механизм защиты от гонок данных. Они гарантируют, что доступ к их состоянию будет синхронизирован автоматически.
Определение и использование актора
actor BankAccount {
private var balance: Double = 0.0
func deposit(amount: Double) {
balance += amount
print("Баланс после пополнения: \(balance)")
}
func withdraw(amount: Double) -> Bool {
guard balance >= amount else { return false }
balance -= amount
return true
}
func getBalance() -> Double { balance }
}
Task {
let account = BankAccount()
await account.deposit(amount: 100)
let success = await account.withdraw(amount: 50)
print("Снятие успешно: \(success)")
}
Все методы актора по умолчанию изолированы — вызывать их можно только через await. Это исключает случайное изменение данных из нескольких потоков.
Неизолированные методы (nonisolated)
actor Logger {
nonisolated func log(message: String) {
print("Лог: \(message)")
}
}
// Вызов без await
let logger = Logger()
logger.log(message: "Тестовое сообщение")
Используйте nonisolated для методов, которые не обращаются к изменяемому состоянию актора.
4. Практические советы и ловушки
- Не блокируйте main thread. Всегда выносите тяжелые вычисления (сеть, базы данных, обработка изображений) в фоновые очереди или асинхронные функции.
- Избегайте deadlock'ов. Не вызывайте
.syncна той же очереди, которая уже выполняется — это приведет к взаимной блокировке. - Используйте
@MainActorдля обновления UI. Пометьте класс или метод этим атрибутом, чтобы гарантировать выполнение на главном потоке. - Не злоупотребляйте глобальными переменными. Если нужно общее состояние, используйте акторы или сериализованные очереди.
- Тестируйте с Thread Sanitizer. Включите санитайзер в Xcode (Product → Scheme → Edit Scheme → Diagnostics → Thread Sanitizer) для выявления гонок данных.
Заключение
Конкурентность в Swift — это не просто набор инструментов, а целая парадигма. От GCD до async/await и Actors — каждый уровень абстракции решает свои задачи. На продвинутом уровне важно не только знать синтаксис, но и понимать, как работают очереди, планировщики и изоляция данных. Используйте акторы для потокобезопасности, async/await для читаемости и GCD для тонкой настройки. Следуя этим принципам, вы сможете писать быстрые, стабильные и масштабируемые приложения для iOS и macOS.