Как использовать DispatchGroup для синхронизации асинхронных задач в Swift?

«Как использовать DispatchGroup для синхронизации асинхронных задач в Swift?» — вопрос из категории Многопоточность, который задают на 10% собеседований IOS Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

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")
    }
}

Важные особенности и лучшие практики:

  1. Всегда используйте defer для group.leave() — гарантирует вызов даже при ошибке
  2. Балансируйте enter() и leave() — несоответствие вызовет deadlock
  3. Избегайте блокировок главного потока — используйте notify вместо wait на main queue
  4. Обрабатывайте таймауты — group.wait(timeout:) предотвращает вечное ожидание
  5. Комбинируйте с другими механизмами — семафорами для ограничения параллелизма

Распространенные ошибки:

// ОШИБКА: Несбалансированные 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