Реальные примеры использования GCD в сложных сценариях

Grand Central Dispatch (GCD) — это мощный инструмент, но его настоящий потенциал раскрывается при решении сложных задач многопоточности. В этой статье мы рассмотрим практические примеры использования GCD в реальных проектах и сценариях, с которыми сталкиваются iOS-разработчики в повседневной работе.


Композиция изображений в реальном времени

Одна из распространенных задач — обработка и композиция изображений для фото-приложений. Вот пример реализации функциональности, подобной фильтрам в Instagram:

class PhotoProcessor {
    private let processingQueue = DispatchQueue(label: "com.photoapp.processing", qos: .userInitiated, attributes: .concurrent)
    private let renderingQueue = DispatchQueue(label: "com.photoapp.rendering", qos: .userInteractive)

    func applyFilters(to image: UIImage, filters: [ImageFilter], completion: @escaping (UIImage?) -> Void) {
        // Создаем группу для отслеживания завершения всех операций фильтрации
        let group = DispatchGroup()
        // Массив для хранения промежуточных результатов
        var filteredImages = [Int: UIImage]()
        let lockQueue = DispatchQueue(label: "com.photoapp.filterlock")

        // Применяем каждый фильтр на processingQueue
        for (index, filter) in filters.enumerated() {
            group.enter()

            processingQueue.async {
                // Применяем фильтр
                guard let filteredImage = filter.apply(to: image) else {
                    group.leave()
                    return
                }

                // Сохраняем результат с синхронизацией доступа
                lockQueue.sync {
                    filteredImages[index] = filteredImage
                }

                group.leave()
            }
        }

        // После завершения всех фильтров, компонуем их вместе
        group.notify(queue: renderingQueue) {
            // Проверяем, что все фильтры успешно применены
            guard filteredImages.count == filters.count else {
                completion(nil)
                return
            }

            // Компонуем слои в правильном порядке
            var compositedImage = image
            for i in 0..<filters.count {
                guard let layerImage = filteredImages[i] else { continue }
                compositedImage = self.compositeImages(base: compositedImage, overlay: layerImage)
            }

            // Возвращаем результат в главный поток
            DispatchQueue.main.async {
                completion(compositedImage)
            }
        }
    }

    private func compositeImages(base: UIImage, overlay: UIImage) -> UIImage {
        // Реализация наложения изображений
        // ...
        return base
    }
}

Ключевые аспекты реализации:

  • Использование отдельной очереди с высоким приоритетом для обработки
  • Применение DispatchGroup для координации параллельных задач
  • Защита доступа к общему состоянию с помощью отдельной lockQueue
  • Компоновка результатов на высокоприоритетной очереди рендеринга
  • Возврат готового результата в главный поток

Кэширование и асинхронная загрузка изображений

Реализация эффективного менеджера изображений с кэшированием — еще один распространенный сценарий:

class ImageManager {
    static let shared = ImageManager()

    private let downloadQueue = DispatchQueue(label: "com.imagemanager.download", qos: .utility, attributes: .concurrent)
    private let processingQueue = DispatchQueue(label: "com.imagemanager.processing", qos: .userInitiated, attributes: .concurrent)
    private let cachingQueue = DispatchQueue(label: "com.imagemanager.caching", qos: .background)

    // Кэш загруженных изображений
    private let imageCache = NSCache<NSString, UIImage>()

    // Словарь для отслеживания активных загрузок, чтобы не дублировать запросы
    private var activeDownloads = [URL: [DownloadCompletion]]()
    private let downloadsLock = NSLock()

    typealias DownloadCompletion = (UIImage?) -> Void

    func downloadImage(from url: URL, completion: @escaping DownloadCompletion) {
        // Проверяем кэш
        if let cachedImage = imageCache.object(forKey: url.absoluteString as NSString) {
            DispatchQueue.main.async {
                completion(cachedImage)
            }
            return
        }

        // Проверяем, не загружается ли уже изображение
        downloadsLock.lock()
        if var callbacks = activeDownloads[url] {
            // Добавляем новый completion в список и выходим
            callbacks.append(completion)
            activeDownloads[url] = callbacks
            downloadsLock.unlock()
            return
        } else {
            // Начинаем новую загрузку
            activeDownloads[url] = [completion]
            downloadsLock.unlock()
        }

        // Выполняем загрузку в фоновой очереди
        downloadQueue.async {
            do {
                let data = try Data(contentsOf: url)

                // Обработка изображения в очереди обработки
                self.processingQueue.async {
                    guard let image = UIImage(data: data) else {
                        self.completeDownload(for: url, with: nil)
                        return
                    }

                    // Оптимизация изображения для отображения
                    let optimizedImage = self.optimizeForDisplay(image)

                    // Сохраняем в кэш
                    self.cachingQueue.async {
                        self.imageCache.setObject(optimizedImage, forKey: url.absoluteString as NSString)
                    }

                    // Уведомляем всех ожидающих
                    self.completeDownload(for: url, with: optimizedImage)
                }
            } catch {
                self.completeDownload(for: url, with: nil)
            }
        }
    }

    private func completeDownload(for url: URL, with image: UIImage?) {
        // Получаем список ожидающих колбэков
        downloadsLock.lock()
        guard let callbacks = activeDownloads[url] else {
            downloadsLock.unlock()
            return
        }

        // Удаляем из активных загрузок
        activeDownloads.removeValue(forKey: url)
        downloadsLock.unlock()

        // Уведомляем всех в главном потоке
        DispatchQueue.main.async {
            for callback in callbacks {
                callback(image)
            }
        }
    }

    private func optimizeForDisplay(_ image: UIImage) -> UIImage {
        // Реализация оптимизации изображения
        // Изменение размера, формата и т.д.
        return image
    }
}

Особенности этой реализации:

  • Разделение задач по отдельным очередям с разными QoS
  • Предотвращение дублирования загрузок одного и того же URL
  • Многоуровневая обработка: загрузка → оптимизация → кэширование
  • Синхронизация доступа к состоянию с помощью NSLock

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

Бизнес-приложения часто требуют фонового обновления данных с различной периодичностью:

class DataSynchronizer {
    private var timer: DispatchSourceTimer?
    private let syncQueue = DispatchQueue(label: "com.app.datasync", qos: .utility)
    private let activeSessionsSemaphore = DispatchSemaphore(value: 3) // Ограничиваем до 3 одновременных сессий

    // Адаптивный интервал, который изменяется в зависимости от активности пользователя
    private var syncInterval: TimeInterval = 300 // 5 минут по умолчанию

    func startSyncProcess() {
        stopSyncProcess() // Останавливаем, если уже запущен

        timer = DispatchSource.makeTimerSource(queue: syncQueue)

        // Настраиваем таймер с небольшим интервалом погрешности для экономии энергии
        timer?.schedule(deadline: .now(), repeating: syncInterval, leeway: .seconds(5))

        timer?.setEventHandler { [weak self] in
            guard let self = self else { return }

            // Проверяем, активно ли приложение для регулировки интервала
            let isActive = self.isAppInForeground()
            self.syncInterval = isActive ? 60 : 300 // 1 минута при активном использовании, иначе 5 минут

            // Ожидаем доступности слота для синхронизации
            self.activeSessionsSemaphore.wait()

            // Выполняем синхронизацию
            self.performSync { success in
                // Освобождаем слот, независимо от результата
                self.activeSessionsSemaphore.signal()

                if !success {
                    // В случае ошибки, увеличиваем интервал с экспоненциальной задержкой
                    self.handleSyncFailure()
                } else {
                    // Сбрасываем счетчик ошибок после успешной синхронизации
                    self.resetErrorCounter()
                }
            }
        }

        timer?.resume()
    }

    func stopSyncProcess() {
        timer?.cancel()
        timer = nil
    }

    private var failureCount = 0

    private func handleSyncFailure() {
        failureCount += 1
        // Экспоненциальное увеличение интервала (но не более 30 минут)
        let maxInterval: TimeInterval = 30 * 60
        syncInterval = min(maxInterval, syncInterval * pow(2, Double(min(failureCount, 5))))
    }

    private func resetErrorCounter() {
        failureCount = 0
    }

    private func performSync(completion: @escaping (Bool) -> Void) {
        // Реализация фактической синхронизации данных
        // ...

        // Для примера:
        DispatchQueue.global().asyncAfter(deadline: .now() + .seconds(Int.random(in: 1...3))) {
            let success = Bool.random()
            completion(success)
        }
    }

    private func isAppInForeground() -> Bool {
        // Проверка активности приложения
        return UIApplication.shared.applicationState == .active
    }
}

Ключевые аспекты реализации:

  • Использование DispatchSourceTimer для точного управления интервалами
  • Семафор для ограничения количества одновременных сессий
  • Адаптивное изменение интервала в зависимости от состояния приложения
  • Экспоненциальная задержка при ошибках (backoff strategy)

Отложенный поиск при вводе текста

Реализация поиска с задержкой для предотвращения частых запросов при быстром вводе:

class DebouncedSearchController {
    private let searchQueue = DispatchQueue(label: "com.app.search", qos: .userInitiated)
    private var pendingWorkItem: DispatchWorkItem?
    private var lastSearchTerm: String?

    typealias SearchCompletion = ([SearchResult]) -> Void

    func search(term: String, debounceTime: TimeInterval = 0.3, completion: @escaping SearchCompletion) {
        // Отменяем предыдущую задачу, если она еще не началась
        pendingWorkItem?.cancel()

        // Если новый запрос такой же, как последний выполненный, пропускаем
        if term == lastSearchTerm {
            return
        }

        // Создаем новую задачу поиска
        let workItem = DispatchWorkItem { [weak self] in
            guard let self = self else { return }

            // Запоминаем термин поиска
            self.lastSearchTerm = term

            // Выполняем поиск (в реальном приложении это может быть запрос к API или базе данных)
            self.performSearch(term) { results in
                // Возвращаем результаты в главном потоке
                DispatchQueue.main.async {
                    completion(results)
                }
            }
        }

        // Сохраняем ссылку на задачу
        pendingWorkItem = workItem

        // Откладываем выполнение задачи
        searchQueue.asyncAfter(deadline: .now() + debounceTime, execute: workItem)
    }

    private func performSearch(_ term: String, completion: @escaping SearchCompletion) {
        // Имитация поискового запроса
        // В реальном приложении здесь был бы запрос к API или базе данных

        // Для демонстрации используем задержку
        Thread.sleep(forTimeInterval: 0.5)

        let results: [SearchResult] = [
            SearchResult(title: "Результат 1 для (term)"),
            SearchResult(title: "Результат 2 для (term)")
        ]

        completion(results)
    }
}

struct SearchResult {
    let title: String
    // Другие поля результата поиска
}

Использование в UISearchController:

class SearchViewController: UIViewController, UISearchBarDelegate {
    private let searchController = UISearchController(searchResultsController: nil)
    private let debouncedSearch = DebouncedSearchController()
    private var searchResults: [SearchResult] = []

    override func viewDidLoad() {
        super.viewDidLoad()

        setupSearchController()
    }

    private func setupSearchController() {
        searchController.searchBar.delegate = self
        navigationItem.searchController = searchController
    }

    // MARK: - UISearchBarDelegate

    func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
        debouncedSearch.search(term: searchText) { [weak self] results in
            guard let self = self else { return }

            self.searchResults = results
            self.tableView.reloadData()
        }
    }
}

Сложная цепочка зависимых задач с обработкой ошибок

Многие бизнес-процессы требуют последовательного выполнения зависимых операций с возможностью обработки ошибок на каждом этапе:

class ComplexWorkflow {
    enum WorkflowError: Error {
        case dataFetchFailed
        case validationFailed
        case processingFailed
        case persistenceFailed
    }

    typealias WorkflowCompletion = (Result<WorkflowResult, Error>) -> Void

    // Зависимые очереди для разных этапов процесса
    private let fetchQueue = DispatchQueue(label: "com.workflow.fetch", qos: .userInitiated)
    private let processingQueue = DispatchQueue(label: "com.workflow.processing", qos: .utility)
    private let persistenceQueue = DispatchQueue(label: "com.workflow.persistence", qos: .background)

    func executeWorkflow(input: WorkflowInput, completion: @escaping WorkflowCompletion) {
        // Шаг 1: Загрузка данных
        fetchQueue.async { [weak self] in
            guard let self = self else { return }

            do {
                let data = try self.fetchData(input: input)

                // Шаг 2: Валидация и обработка данных
                self.processingQueue.async {
                    do {
                        let validatedData = try self.validateData(data)
                        let processedData = try self.processData(validatedData)

                        // Шаг 3: Сохранение результатов
                        self.persistenceQueue.async {
                            do {
                                let result = try self.persistResults(processedData)

                                // Успешное завершение всей цепочки
                                DispatchQueue.main.async {
                                    completion(.success(result))
                                }
                            } catch {
                                // Ошибка на этапе сохранения
                                DispatchQueue.main.async {
                                    completion(.failure(WorkflowError.persistenceFailed))
                                }
                            }
                        }
                    } catch let validationError as WorkflowError {
                        // Ошибка на этапе валидации
                        DispatchQueue.main.async {
                            completion(.failure(validationError))
                        }
                    } catch {
                        // Ошибка на этапе обработки
                        DispatchQueue.main.async {
                            completion(.failure(WorkflowError.processingFailed))
                        }
                    }
                }
            } catch {
                // Ошибка на этапе загрузки данных
                DispatchQueue.main.async {
                    completion(.failure(WorkflowError.dataFetchFailed))
                }
            }
        }
    }

    // Реализация отдельных этапов рабочего процесса

    private func fetchData(input: WorkflowInput) throws -> RawData {
        // Симуляция загрузки данных
        return RawData()
    }

    private func validateData(_ data: RawData) throws -> ValidatedData {
        // Проверка данных
        return ValidatedData()
    }

    private func processData(_ data: ValidatedData) throws -> ProcessedData {
        // Обработка данных
        return ProcessedData()
    }

    private func persistResults(_ data: ProcessedData) throws -> WorkflowResult {
        // Сохранение результатов
        return WorkflowResult()
    }
}

// Вспомогательные структуры для примера
struct WorkflowInput {}
struct RawData {}
struct ValidatedData {}
struct ProcessedData {}
struct WorkflowResult {}

Улучшенная версия с использованием DispatchGroup:

func executeWorkflowImproved(input: WorkflowInput, completion: @escaping WorkflowCompletion) {
    let workflowGroup = DispatchGroup()
    var currentResult: Any? = input
    var workflowError: Error?

    // Шаг 1: Загрузка данных
    workflowGroup.enter()
    fetchQueue.async { [weak self] in
        guard let self = self else {
            workflowGroup.leave()
            return
        }

        do {
            let data = try self.fetchData(input: input)
            currentResult = data
        } catch {
            workflowError = error
        }
        workflowGroup.leave()
    }

    // Шаг 2: Обработка данных
    workflowGroup.notify(queue: processingQueue) { [weak self] in
        guard let self = self, workflowError == nil, let data = currentResult as? RawData else {
            workflowGroup.leave()
            return
        }

        workflowGroup.enter()
        do {
            let validated = try self.validateData(data)
            let processed = try self.processData(validated)
            currentResult = processed
        } catch {
            workflowError = error
        }
        workflowGroup.leave()
    }

    // Шаг 3: Сохранение результатов
    workflowGroup.notify(queue: persistenceQueue) { [weak self] in
        guard let self = self, workflowError == nil, let data = currentResult as? ProcessedData else {
            workflowGroup.leave()
            return
        }

        workflowGroup.enter()
        do {
            let result = try self.persistResults(data)
            currentResult = result
        } catch {
            workflowError = error
        }
        workflowGroup.leave()
    }

    // Финальное уведомление
    workflowGroup.notify(queue: .main) {
        if let error = workflowError {
            completion(.failure(error))
        } else if let result = currentResult as? WorkflowResult {
            completion(.success(result))
        } else {
            completion(.failure(WorkflowError.processingFailed))
        }
    }
}

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

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

  1. Concurrency Programming Guide - Apple Developer Documentation
  2. Grand Central Dispatch In-Depth - objc.io
  3. WWDC 2015: Building Responsive and Efficient Apps with GCD
Также читайте:  Как должен мыслить разработчик, при проектировании архитектуры мобльного приложения