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

Ответ

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

Ответ 18+ 🔞

Слушай, а вот эта штука — DispatchGroup — она же, блядь, как тот самый Герасим из рассказа, только для асинхронных задач, понимаешь? Немой, здоровый мужик, который стоит и считает: "Му-му... му-му...". Зашла задача в группу — он говорит "Му!" (это enter()), вышла — "Му!" (это leave()). И когда все "Му-му" закончились, он такой: "Ну всё, пиздуй дальше, я всех посчитал".

Вот смотри, как это выглядит в коде, блядь:

let group = DispatchGroup()
let queue = DispatchQueue.global(qos: .userInitiated)

// Задача первая, сука
group.enter() // Герасим: "Му!"
queue.async {
    defer { group.leave() } // Автоматом скажет "Му!" на выходе, даже если всё пиздец

    Thread.sleep(forTimeInterval: 1)
    print("Первая хуйня сделана")
}

// Задача вторая, ёпта
group.enter() // "Му-му!"
queue.async {
    defer { group.leave() } // Опять "Му!"

    Thread.sleep(forTimeInterval: 2)
    print("Вторая хуйня тоже")
}

// И теперь, блядь, варианта два:
// Либо ждём, как лох, блокируя поток:
group.wait() // Стоим, бля, как вкопанные, пока все "Му-му" не закончатся
print("Всё, приехали (блокирующий вариант)")

// Либо по-умному, через notify:
group.notify(queue: .main) {
    // Это выполнится на главной очереди, когда все "Му-му" уйдут
    print("Всё, приплыли (неблокирующий вариант)")
    self.обновитьИнтерфейс()
}

print("А это выведется сразу, потому что мы не ждём, мы — хитрожопые")

А вот, блядь, где это реально пригождается, на практике:

1. Несколько сетевых запросов параллельно, сука:

func загрузитьВсёХуйню(completion: @escaping ([Data]) -> Void) {
    let group = DispatchGroup()
    var результаты: [Data] = []
    // Очередь для потокобезопасного доступа к массиву, а то налетят пидарасы-гонки данных
    let очередьДляРезультатов = DispatchQueue(label: "com.example.результаты", attributes: .concurrent)

    let урлы = [URL(string: "https://api.example.com/пользователи")!,
                URL(string: "https://api.example.com/посты")!,
                URL(string: "https://api.example.com/комменты")!]

    for url in урлы {
        group.enter() // "Му!"

        URLSession.shared.dataTask(with: url) { data, response, error in
            defer { group.leave() } // "Му!" в любом случае, даже если ошибка

            if let data = data {
                очередьДляРезультатов.async(flags: .barrier) { // Барьер, бля, чтобы не лезли
                    результаты.append(data)
                }
            }
        }.resume()
    }

    group.notify(queue: .main) {
        completion(результаты) // Всё загрузилось, можно отдавать
    }
}

2. Обработка файлов с ограничением по потокам и таймаутом, ёпта:

func обработатьФайлы(_ fileURLs: [URL], таймаут: TimeInterval = 30) {
    let group = DispatchGroup()
    let семафор = DispatchSemaphore(value: 3) // Не больше трёх файлов одновременно, а то комп взвоет

    for fileURL in fileURLs {
        group.enter() // "Му!"

        семафор.wait() // Ждём, если уже три в работе

        DispatchQueue.global().async {
            defer {
                семафор.signal() // Освобождаем слот
                group.leave()    // "Му!"
            }

            do {
                let data = try Data(contentsOf: fileURL)
                // Что-то делаем с файлом
                обработатьФайл(data)
            } catch {
                print("Файл (fileURL) оказался говном: (error)")
            }
        }
    }

    // Ждём, но не вечно, блядь
    let результатОжидания = group.wait(timeout: .now() + таймаут)

    switch результатОжидания {
    case .success:
        print("Все файлы обработаны, молодцы")
    case .timedOut:
        print("Таймаут, ёпта! Какие-то файлы ещё в процессе, но нам уже похуй")
        // Тут можно попробовать отменить оставшиеся операции
    }
}

3. Комбинация с OperationQueue, чтоб им всем пусто было:

func качнутьКартинки(_ imageURLs: [URL]) {
    let group = DispatchGroup()
    let operationQueue = OperationQueue()
    operationQueue.maxConcurrentOperationCount = 4 // Не больше четырёх параллельно

    var обработанныеКартинки: [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 {
                        обработанныеКартинки.append(image) // На главный поток для UI
                    }
                }
            } catch {
                print("Не удалось скачать картинку с (url), пидарас какой-то")
            }
        }

        operationQueue.addOperation(operation)
    }

    group.notify(queue: .main) {
        print("Скачано картинок: (обработанныеКартинки.count)")
        self.показатьКартинки(обработанныеКартинки)
    }
}

4. Вложенные группы, ёбаный в рот! Это когда Герасим считает "Му-му", а внутри каждого "Му" ещё свои "Му-му-му":

func сложныйСценарий() {
    let внешняяГруппа = DispatchGroup()
    let внутренняяГруппа = DispatchGroup()

    // Этап 1: Подготовка данных
    внешняяГруппа.enter()
    подготовитьДанные { data in
        defer { внешняяГруппа.leave() }

        // Этап 2: Параллельная обработка кусков
        for chunk in data.chunks {
            внутренняяГруппа.enter()

            обработатьКусок(chunk) { result in
                // Что-то делаем с результатом
                внутренняяГруппа.leave()
            }
        }

        // Ждём завершения всех кусков
        внутренняяГруппа.notify(queue: .global()) {
            print("Все куски обработаны, можно выдыхать")
        }
    }

    внешняяГруппа.notify(queue: .main) {
        print("Весь сложный сценарий выполнен, охуенно!")
    }
}

Важные моменты, блядь, чтобы не обосраться:

  1. Всегда defer { group.leave() } — это как страховка, чтобы "Му!" сказалось даже если в середине операции пиздец.
  2. Баланс enter() и leave() — если "Му!" сказал, а "Му!" не ответил, группа будет ждать вечно, как дурак.
  3. Не блокируй главный поток wait()-ом — используй notify, а то UI замрёт, и пользователь пошлёт тебя нахуй.
  4. Таймауты, сука!group.wait(timeout:) чтобы не ждать до второго пришествия.
  5. Комбинируй с семафорами — если нужно ограничить одновременное выполнение, как в примере с файлами.

Типичные косяки, ёпта:

// КОСЯК: enter без leave
let group = DispatchGroup()
group.enter()
DispatchQueue.global().async {
    // Забыли group.leave() — вечный дедлок, Герасим будет ждать своего "Му!" до скончания веков
}
group.wait() // Тут зависнем нахуй

// КОСЯК: leave без enter
group.leave() // Краш: over-released, счётчик ушёл в минус

// ПРАВИЛЬНО: defer, блядь!
func безопаснаяРабота(group: DispatchGroup) {
    group.enter()

    defer { group.leave() } // Сработает всегда, даже если ниже всё полетит к чертям

    let результат = try? рискованнаяОперация()
    // Что-то делаем...
}

Чем ещё можно заменить, если DispatchGroup надоел:

  • OperationQueue с зависимостями — для сложных цепочек, где одна операция ждёт другую.
  • Async/Await (Swift 5.5+) — современно, красиво, но не везде доступно.
  • PromiseKit и аналоги — через промисы/фьючерсы.
  • Combine — если ты любишь реактивщину и боль, блядь.

Когда DispatchGroup — твой выбор:

  • Нужно просто дождаться кучки асинхронных задач.
  • Мигрируешь старый код на GCD.
  • Не хочешь тянуть лишние зависимости.
  • Когда async/await ещё нельзя использовать, потому что проект на старом свифте, ёпта.

В общем, DispatchGroup — это как тот самый Герасим: простой, немой, но охуенно эффективный, когда нужно посчитать все "Му-му" и сказать, что всё готово. Главное — не забывать отвечать ему "Му!" на каждый его "Му!", а то он обидится и зависнет навеки.