Группы диспетчеризации (Dispatch Groups) в GCD

При работе с асинхронными задачами часто возникает необходимость отслеживать завершение группы связанных операций. Dispatch Groups в Grand Central Dispatch предоставляют элегантный механизм для координации нескольких асинхронных задач и синхронизации кода. Эта статья раскрывает все аспекты работы с группами диспетчеризации в iOS-приложениях.


Основы работы с Dispatch Groups

Dispatch Groups позволяют группировать задачи и получать уведомление, когда все задачи в группе завершены, даже если они выполняются на разных очередях.

Создание и базовое использование

// Создание группы
let group = DispatchGroup()

// Входим в группу перед началом задачи
group.enter()
someAsyncOperation {
    // Выполнение задачи
    print("Задача 1 завершена")
    // Выходим из группы после завершения
    group.leave()
}

// Добавление ещё одной задачи
group.enter()
anotherAsyncOperation {
    print("Задача 2 завершена")
    group.leave()
}

// Уведомление о завершении всех задач
group.notify(queue: .main) {
    print("Все задачи завершены")
    updateUI()
}

Ключевые принципы:

  • Вызов enter() увеличивает внутренний счетчик группы
  • Вызов leave() уменьшает счетчик
  • Когда счетчик достигает нуля, вызывается блок notify

Явное добавление задач в группу

Помимо пары enter()/leave(), GCD предлагает более удобный способ добавления задач в группу:

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

// Добавление задачи напрямую
queue.async(group: group) {
    print("Задача 1 выполняется")
    sleep(2)
    print("Задача 1 завершена")
}

queue.async(group: group) {
    print("Задача 2 выполняется")
    sleep(1)
    print("Задача 2 завершена")
}

group.notify(queue: .main) {
    print("Все задачи завершены")
}

Синхронное ожидание завершения группы

Иногда требуется заблокировать текущий поток до завершения всех задач в группе. GCD предоставляет метод wait для этой цели.

Блокирующее ожидание

let group = DispatchGroup()
let queue = DispatchQueue.global()

// Добавляем задачи в группу
queue.async(group: group) {
    sleep(2)
    print("Задача 1 завершена")
}

queue.async(group: group) {
    sleep(1)
    print("Задача 2 завершена")
}

// Блокируем текущий поток до завершения всех задач
group.wait()
print("После ожидания - все задачи завершены")

Ожидание с таймаутом

Для предотвращения бесконечного блокирования можно установить таймаут:

let result = group.wait(timeout: .now() + 3)

switch result {
case .success:
    print("Все задачи завершены в течение таймаута")
case .timedOut:
    print("Таймаут: не все задачи успели завершиться")
}

Важно: Не используйте wait() на главной очереди, так как это приведет к блокировке пользовательского интерфейса.

Вложенные группы и сложные зависимости

Dispatch Groups можно комбинировать для создания сложных зависимостей между задачами.

Последовательное выполнение групп задач

let firstGroup = DispatchGroup()
let secondGroup = DispatchGroup()
let queue = DispatchQueue.global()

// Первая группа задач
queue.async(group: firstGroup) {
    print("Задача 1.1 выполняется")
    sleep(1)
    print("Задача 1.1 завершена")
}

queue.async(group: firstGroup) {
    print("Задача 1.2 выполняется")
    sleep(2)
    print("Задача 1.2 завершена")
}

// Вторая группа запускается после завершения первой
firstGroup.notify(queue: queue) {
    print("Первая группа задач завершена")

    // Задачи второй группы
    queue.async(group: secondGroup) {
        print("Задача 2.1 выполняется")
        sleep(1)
        print("Задача 2.1 завершена")
    }

    queue.async(group: secondGroup) {
        print("Задача 2.2 выполняется")
        sleep(1)
        print("Задача 2.2 завершена")
    }

    // Уведомление о завершении второй группы
    secondGroup.notify(queue: .main) {
        print("Все группы задач завершены")
        updateFinalUI()
    }
}

Параллельное выполнение групп с финальной синхронизацией

let groupA = DispatchGroup()
let groupB = DispatchGroup()
let finalGroup = DispatchGroup()
let queue = DispatchQueue.global()

// Добавляем финальную группу для отслеживания завершения
finalGroup.enter()
finalGroup.enter()

// Задачи группы A
queue.async(group: groupA) {
    sleep(2)
    print("Группа A: задача завершена")
}

// Уведомление о завершении группы A
groupA.notify(queue: queue) {
    print("Группа A полностью завершена")
    finalGroup.leave()
}

// Задачи группы B
queue.async(group: groupB) {
    sleep(1)
    print("Группа B: задача завершена")
}

// Уведомление о завершении группы B
groupB.notify(queue: queue) {
    print("Группа B полностью завершена")
    finalGroup.leave()
}

// Финальное уведомление, когда обе группы A и B завершены
finalGroup.notify(queue: .main) {
    print("Обе группы A и B завершены")
    showFinalResult()
}

Практические сценарии использования

Dispatch Groups особенно полезны в ряде типичных сценариев iOS-разработки.

Параллельные сетевые запросы

func loadDashboardData(completion: @escaping (Result<DashboardData, Error>) -> Void) {
    let group = DispatchGroup()
    let queue = DispatchQueue.global(qos: .userInitiated)

    var userData: UserData?
    var postsData: [Post]?
    var notificationsData: [Notification]?
    var error: Error?

    // Загрузка данных пользователя
    group.enter()
    networkService.fetchUserData { result in
        defer { group.leave() }
        switch result {
        case .success(let data):
            userData = data
        case .failure(let err):
            error = err
        }
    }

    // Загрузка постов
    group.enter()
    networkService.fetchPosts { result in
        defer { group.leave() }
        switch result {
        case .success(let data):
            postsData = data
        case .failure(let err):
            error = err
        }
    }

    // Загрузка уведомлений
    group.enter()
    networkService.fetchNotifications { result in
        defer { group.leave() }
        switch result {
        case .success(let data):
            notificationsData = data
        case .failure(let err):
            error = err
        }
    }

    // Обработка результатов всех запросов
    group.notify(queue: .main) {
        if let error = error {
            completion(.failure(error))
            return
        }

        guard let user = userData, let posts = postsData, let notifications = notificationsData else {
            completion(.failure(DashboardError.incompleteData))
            return
        }

        let dashboardData = DashboardData(user: user, posts: posts, notifications: notifications)
        completion(.success(dashboardData))
    }
}

Обработка массива данных с ограничением параллельности

func processImages(_ images: [UIImage], completion: @escaping ([UIImage]) -> Void) {
    let group = DispatchGroup()
    let queue = DispatchQueue(label: "com.myapp.imageProcessing", attributes: .concurrent)
    let resultLock = NSLock()
    var processedImages = [UIImage?](repeating: nil, count: images.count)

    // Обработка каждого изображения в параллельной очереди
    for (index, image) in images.enumerated() {
        queue.async(group: group) {
            let processedImage = self.applyFilter(to: image)

            // Безопасное сохранение результата
            resultLock.lock()
            processedImages[index] = processedImage
            resultLock.unlock()
        }
    }

    // Уведомление о завершении всех операций
    group.notify(queue: .main) {
        let nonNilImages = processedImages.compactMap { $0 }
        completion(nonNilImages)
    }
}

Не забудьте загрузить приложение iJun в AppStore и подписаться на мой YouTube канал для получения дополнительных материалов по iOS-разработке.

Список литературы и дополнительных материалов

  1. DispatchGroup - Apple Developer Documentation
  2. Concurrency Programming Guide - Apple Developer Documentation
  3. WWDC 2016: Concurrent Programming With GCD in Swift 3
Также читайте:  Семафоры и другие примитивы синхронизации в GCD