Введение: Зачем нужны очереди задач в Swift?
В современной разработке под iOS и macOS производительность приложения напрямую зависит от того, насколько эффективно вы управляете потоками. Длительные операции — загрузка данных из сети, обработка изображений, работа с базой данных — не должны блокировать главный поток (Main Thread), отвечающий за интерфейс. Если главный поток занят, пользователь видит "зависший" экран, а система может принудительно завершить приложение.
Очереди задач (Task Queues) решают эту проблему. Они позволяют выполнять код асинхронно, параллельно или последовательно, не блокируя UI. В Swift для этого есть три основных инструмента: Grand Central Dispatch (GCD), OperationQueue и современная модель async/await (Swift Concurrency). В этой статье мы разберем их на практических примерах и выясним, когда что использовать.
1. Grand Central Dispatch (GCD): классический подход
GCD — это низкоуровневая библиотека на C, которая управляет пулом потоков. В Swift с ней удобно работать через глобальные очереди (DispatchQueue).
1.1. Основные типы очередей в GCD
- Main Queue — последовательная очередь главного потока. Используется для обновления UI.
- Global Queues — параллельные очереди с разным приоритетом (QoS:
.userInteractive,.userInitiated,.utility,.background). - Custom Queues — созданные вами очереди (могут быть последовательными или параллельными).
1.2. Пример: загрузка данных в фоне и обновление UI
Допустим, нам нужно загрузить JSON с удаленного сервера и отобразить результат в таблице.
DispatchQueue.global(qos: .userInitiated).async {
// 1. Фоновая работа: загрузка данных
guard let url = URL(string: "https://api.example.com/data") else { return }
let data = try? Data(contentsOf: url)
// 2. Возвращаемся на главный поток для обновления UI
DispatchQueue.main.async {
// Обновляем модель данных или таблицу
self.tableView.reloadData()
}
}
1.3. Использование DispatchGroup для синхронизации
Когда нужно выполнить несколько параллельных задач и дождаться их завершения, используйте DispatchGroup.
let group = DispatchGroup()
var images: [UIImage] = []
for url in imageURLs {
group.enter()
DispatchQueue.global().async {
if let data = try? Data(contentsOf: url), let image = UIImage(data: data) {
images.append(image)
}
group.leave()
}
}
group.notify(queue: .main) {
print("Все изображения загружены: \(images.count)")
// Обновляем UI
}
2. OperationQueue: более высокий уровень абстракции
OperationQueue построена поверх GCD, но предоставляет дополнительные возможности: отмена задач, зависимости между операциями, контроль максимального количества параллельных операций.
2.1. Создание и запуск операций
Операции можно создавать через BlockOperation или наследуясь от Operation.
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 2 // Ограничиваем до 2 параллельных задач
let operation1 = BlockOperation {
print("Операция 1 началась")
sleep(2)
print("Операция 1 завершена")
}
let operation2 = BlockOperation {
print("Операция 2 началась")
sleep(1)
print("Операция 2 завершена")
}
// Устанавливаем зависимость: operation2 начнется только после operation1
operation2.addDependency(operation1)
queue.addOperations([operation1, operation2], waitUntilFinished: false)
2.2. Отмена операций
В отличие от GCD, операции можно отменять. Это критично, если пользователь ушел с экрана, а загрузка все еще идет.
// Создаем операцию, которая проверяет флаг isCancelled
let cancellableOperation = BlockOperation {
for i in 0..<100 {
if cancellableOperation.isCancelled {
print("Операция отменена на шаге \(i)")
return
}
print("Шаг \(i)")
sleep(1)
}
}
queue.addOperation(cancellableOperation)
// Отменяем через 3 секунды
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
cancellableOperation.cancel()
}
3. Swift Concurrency (async/await): новый стандарт
Начиная с iOS 13 (Swift 5.5), Apple рекомендует использовать async/await и Task. Это делает асинхронный код таким же простым, как синхронный, и автоматически управляет потоками.
3.1. Базовый пример с async/await
func fetchData() async throws -> Data {
let url = URL(string: "https://api.example.com/data")!
let (data, _) = try await URLSession.shared.data(from: url)
return data
}
// Вызов из ViewController
Task {
do {
let data = try await fetchData()
// Автоматически возвращаемся на главный поток
self.label.text = "Данные загружены: \(data.count) байт"
} catch {
print("Ошибка: \(error)")
}
}
3.2. Параллельное выполнение с async let
Если нужно загрузить несколько файлов одновременно, используйте async let.
func loadMultipleImages() async throws -> [UIImage] {
async let image1 = loadImage(from: url1)
async let image2 = loadImage(from: url2)
async let image3 = loadImage(from: url3)
// Все три загрузки выполняются параллельно
return try await [image1, image2, image3]
}
func loadImage(from url: URL) async throws -> UIImage {
let (data, _) = try await URLSession.shared.data(from: url)
guard let image = UIImage(data: data) else {
throw URLError(.badServerResponse)
}
return image
}
3.3. Акторы (Actors) для потокобезопасности
При работе с очередями задач часто возникает проблема гонки данных (data race). Swift Concurrency решает её через actor — потокобезопасный тип.
actor Counter {
private var value = 0
func increment() {
value += 1
}
func getValue() -> Int {
return value
}
}
let counter = Counter()
Task {
await counter.increment()
print(await counter.getValue())
}