Введение в очереди задач в Swift
Мобильные и десктопные приложения на Swift часто выполняют несколько операций одновременно: загрузка данных из сети, обработка изображений, анимация интерфейса. Без правильного управления этими задачами приложение может "зависнуть" или тормозить. Очереди задач — это механизм, который позволяет организовать выполнение кода в фоновом режиме, не блокируя главный поток (UI).
В Swift существует три основных подхода к работе с очередями:
- Grand Central Dispatch (GCD) — низкоуровневый API для управления очередями.
- OperationQueue — более высокоуровневая абстракция над GCD.
- async/await (Swift 5.5+) — современный асинхронный синтаксис.
В этой статье мы разберём каждый подход на практических примерах, понятных даже новичкам.
1. Grand Central Dispatch (GCD) — основа очередей
GCD — это библиотека Apple, которая управляет пулом потоков. Вы просто создаёте очередь и отправляете в неё задачи. Система сама решает, на каком потоке выполнять код.
Типы очередей в GCD
- Main Queue — выполняет задачи на главном потоке (только UI-обновления).
- Global Queues — фоновые очереди с разным приоритетом (userInteractive, userInitiated, utility, background).
- Private Queues — ваши собственные очереди, которые можно настроить как последовательные (serial) или параллельные (concurrent).
Пример: загрузка данных в фоне
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var imageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
// Запускаем задачу в фоновой очереди
DispatchQueue.global(qos: .userInitiated).async {
let url = URL(string: "https://example.com/image.jpg")!
if let data = try? Data(contentsOf: url),
let image = UIImage(data: data) {
// Возвращаемся на главный поток для обновления UI
DispatchQueue.main.async {
self.imageView.image = image
}
}
}
}
}
Последовательные vs параллельные очереди
Последовательная очередь выполняет задачи одну за другой. Параллельная — может запускать несколько задач одновременно.
// Последовательная очередь
let serialQueue = DispatchQueue(label: "com.example.serial")
serialQueue.async { print("Task 1") }
serialQueue.async { print("Task 2") }
// Вывод: Task 1, затем Task 2
// Параллельная очередь
let concurrentQueue = DispatchQueue(label: "com.example.concurrent", attributes: .concurrent)
concurrentQueue.async { print("Task A") }
concurrentQueue.async { print("Task B") }
// Вывод: Task A или Task B (порядок не гарантирован)
2. OperationQueue — управление зависимостями
OperationQueue — это более удобная обёртка над GCD, которая позволяет:
- Устанавливать зависимости между задачами.
- Отменять выполнение задач.
- Управлять максимальным количеством параллельных операций.
Создание и выполнение операций
let operationQueue = OperationQueue()
operationQueue.maxConcurrentOperationCount = 2 // максимум 2 задачи одновременно
let operation1 = BlockOperation {
print("Операция 1 выполняется")
Thread.sleep(forTimeInterval: 1)
}
let operation2 = BlockOperation {
print("Операция 2 выполняется")
Thread.sleep(forTimeInterval: 1)
}
// Устанавливаем зависимость: operation2 начнётся только после operation1
operation2.addDependency(operation1)
operationQueue.addOperations([operation1, operation2], waitUntilFinished: false)
Отмена операций
operationQueue.cancelAllOperations() // Отменяет все ожидающие операции
// Или отмена конкретной операции
operation1.cancel()
3. async/await — современный Swift
Начиная с Swift 5.5, появился встроенный асинхронный синтаксис. Он делает код более читаемым и безопасным.
Базовая асинхронная функция
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: \(user.name)")
} catch {
print("Ошибка: \(error)")
}
}
Параллельные задачи с async/await
// Загружаем несколько ресурсов параллельно
func loadResources() async {
async let image1 = loadImage(from: url1)
async let image2 = loadImage(from: url2)
// Ожидаем завершения обеих задач
let (img1, img2) = await (image1, image2)
// Обновляем UI на главном потоке
await MainActor.run {
self.imageView1.image = img1
self.imageView2.image = img2
}
}
4. Практические советы и лучшие практики
Когда использовать каждый подход?
- GCD — для простых фоновых задач (загрузка данных, обработка файлов).
- OperationQueue — когда нужны зависимости, отмена или ограничение параллелизма.
- async/await — для нового кода (Swift 5.5+), особенно при работе с сетью и файлами.
Избегайте "Race Conditions"
Когда несколько потоков обращаются к одной переменной, используйте семафоры или атомарные операции:
class Counter {
private let queue = DispatchQueue(label: "com.example.counter")
private var value = 0
func increment() {
queue.sync {
value += 1
}
}
func getValue() -> Int {
return queue.sync { value }
}
}
Не блокируйте главный поток
Любые длительные операции (сеть, файлы, вычисления) должны выполняться в фоне. Обновление UI — только на главном потоке.
// НЕПРАВИЛЬНО:
DispatchQueue.main.async {
let data = try! Data(contentsOf: url) // Блокирует UI!
}
// ПРАВИЛЬНО:
DispatchQueue.global().async {
let data = try! Data(contentsOf: url)
DispatchQueue.main.async {
// Обновляем UI
}
}