Ответ
DispatchGroup — это механизм в Grand Central Dispatch (GCD) для синхронизации выполнения нескольких асинхронных задач. Он позволяет отслеживать завершение группы операций и выполнять код, когда все они закончены.
Базовое использование:
let group = DispatchGroup()
let queue = DispatchQueue.global(qos: .userInitiated)
// Задача 1
group.enter() // Увеличиваем счетчик группы
queue.async {
defer { group.leave() } // Уменьшаем счетчик при выходе
// Длительная операция
Thread.sleep(forTimeInterval: 1)
print("Task 1 completed")
}
// Задача 2
group.enter()
queue.async {
defer { group.leave() }
Thread.sleep(forTimeInterval: 2)
print("Task 2 completed")
}
// Ожидаем завершения всех задач
group.wait() // Блокирует текущий поток
print("All tasks completed (blocking wait)")
// ИЛИ используем notify для неблокирующего ожидания
group.notify(queue: .main) {
// Выполняется на главной очереди после всех задач
print("All tasks completed (non-blocking)")
self.updateUI()
}
print("This executes immediately (with notify)")
Практические примеры использования:
1. Параллельные сетевые запросы:
func fetchMultipleResources(completion: @escaping ([Data]) -> Void) {
let group = DispatchGroup()
var results: [Data] = []
let urls = [
URL(string: "https://api.example.com/users")!,
URL(string: "https://api.example.com/posts")!,
URL(string: "https://api.example.com/comments")!
]
// Для потокобезопасного доступа к массиву
let resultsQueue = DispatchQueue(label: "com.example.results",
attributes: .concurrent)
for url in urls {
group.enter()
URLSession.shared.dataTask(with: url) { data, response, error in
defer { group.leave() }
if let data = data {
resultsQueue.async(flags: .barrier) {
results.append(data)
}
}
}.resume()
}
group.notify(queue: .main) {
completion(results)
}
}
2. Обработка файлов с таймаутом:
func processFiles(_ fileURLs: [URL], timeout: TimeInterval = 30) {
let group = DispatchGroup()
let semaphore = DispatchSemaphore(value: 3) // Ограничиваем до 3 параллельных задач
for fileURL in fileURLs {
group.enter()
// Ограничиваем количество одновременных операций
semaphore.wait()
DispatchQueue.global().async {
defer {
semaphore.signal()
group.leave()
}
do {
let data = try Data(contentsOf: fileURL)
// Обработка файла
processFileData(data)
} catch {
print("Error processing file: (error)")
}
}
}
// Ожидаем с таймаутом
let result = group.wait(timeout: .now() + timeout)
switch result {
case .success:
print("All files processed successfully")
case .timedOut:
print("Timeout: some files are still processing")
// Можно отменить оставшиеся задачи
}
}
3. Комбинирование с OperationQueue:
func downloadAndProcessImages(_ imageURLs: [URL]) {
let group = DispatchGroup()
let operationQueue = OperationQueue()
operationQueue.maxConcurrentOperationCount = 4
var processedImages: [UIImage] = []
for url in imageURLs {
group.enter()
let operation = BlockOperation {
defer { group.leave() }
do {
let data = try Data(contentsOf: url)
if let image = UIImage(data: data) {
// Потокобезопасное добавление
DispatchQueue.main.async {
processedImages.append(image)
}
}
} catch {
print("Failed to download image from (url)")
}
}
operationQueue.addOperation(operation)
}
group.notify(queue: .main) {
print("Downloaded (processedImages.count) images")
self.displayImages(processedImages)
}
}
4. Вложенные DispatchGroup:
func complexWorkflow() {
let outerGroup = DispatchGroup()
let innerGroup = DispatchGroup()
// Этап 1: Подготовка данных
outerGroup.enter()
prepareData { data in
defer { outerGroup.leave() }
// Этап 2: Параллельная обработка частей данных
for chunk in data.chunks {
innerGroup.enter()
processChunk(chunk) { result in
// Обработка результата
innerGroup.leave()
}
}
// Ждем завершения всех chunk-обработок
innerGroup.notify(queue: .global()) {
print("All chunks processed")
}
}
outerGroup.notify(queue: .main) {
print("Complex workflow completed")
}
}
Важные особенности и лучшие практики:
- Всегда используйте defer для group.leave() — гарантирует вызов даже при ошибке
- Балансируйте enter() и leave() — несоответствие вызовет deadlock
- Избегайте блокировок главного потока — используйте notify вместо wait на main queue
- Обрабатывайте таймауты — group.wait(timeout:) предотвращает вечное ожидание
- Комбинируйте с другими механизмами — семафорами для ограничения параллелизма
Распространенные ошибки:
// ОШИБКА: Несбалансированные enter/leave
let group = DispatchGroup()
group.enter()
DispatchQueue.global().async {
// Забыли вызвать group.leave()
// DEADLOCK: группа никогда не завершится
}
group.wait() // Навсегда заблокируется
// ОШИБКА: Вызов leave() без enter()
group.leave() // Crash: over-released
// ПРАВИЛЬНО: Использование defer
func safeAsyncWork(group: DispatchGroup) {
group.enter()
defer { group.leave() } // Гарантированный вызов
// Код, который может выбросить исключение
let result = try riskyOperation()
process(result)
}
Альтернативы DispatchGroup:
- OperationQueue с зависимостями — для сложных workflows
- Async/Await (Swift 5.5+) — более современный подход
- Promise/Future — через библиотеки вроде PromiseKit
- Combine — для реактивного программирования
Когда использовать DispatchGroup:
- Простая синхронизация нескольких асинхронных задач
- Когда нужно дождаться завершения группы операций
- Для миграции legacy-кода на GCD
- Когда нельзя использовать современные async/await