← Назад к вопросам

Что нельзя делать на серийной очереди?

2.0 Middle🔥 161 комментариев
#Многопоточность и асинхронность

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI6 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Общие ограничения серийных очередей в iOS

На серийной очереди (serial queue) в GCD (Grand Central Dispatch) существует одно фундаментальное ограничение, которое определяет все остальные правила работы с ней:

Нельзя выполнять синхронные вызовы (sync) из задач, уже находящихся на этой же серийной очереди

Это приводит к взаимной блокировке (deadlock). Серийная очередь обрабатывает задачи строго последовательно, одна за другой. Когда вы вызываете dispatch_sync (или sync в Swift) на той же очереди, вы ставите новую задачу в конец очереди и блокируете текущий поток до её выполнения. Но поскольку очередь серийная, она не может начать выполнение новой задачи, пока не завершит текущую. А текущая задача как раз и ждёт завершения этой новой задачи. Возникает классический deadlock.

let serialQueue = DispatchQueue(label: "com.example.serial")

// ПРИМЕР, КОТОРЫЙ ВЫЗОВЕТ DEADLOCK:
serialQueue.async {
    print("Начало внешней задачи")
    
    // ОПАСНО: синхронный вызов на той же серийной очереди
    serialQueue.sync {
        print("Эта строка никогда не выполнится")
    }
    
    print("Конец внешней задачи - тоже никогда не выполнится")
}

Конкретные запрещённые действия

  1. Синхронное ожидание завершения задачи на той же очереди (описано выше)
  2. Синхронный вызов метода, который сам использует sync на этой очереди (косвенная блокировка)
  3. Необдуманное использование .barrier флагов на неконкурентных очередях — барьеры имеют смысл только для конкурентных очередей, на серийных они избыточны и могут вводить в заблуждение.
// Пример косвенной блокировки через вложенные вызовы
class ProblematicManager {
    let serialQueue = DispatchQueue(label: "com.example.manager")
    private var value = 0
    
    func setValue(_ newValue: Int) {
        serialQueue.sync {
            value = newValue
        }
    }
    
    func increment() {
        serialQueue.async { [weak self] in
            guard let self = self else { return }
            // ОПАСНО: вызов sync из задачи, выполняющейся на serialQueue
            let current = self.value // Допустимо
            self.setValue(current + 1) // Потенциальный deadlock!
        }
    }
}

Рекомендации по безопасной работе

  • Для реентерабельного кода используйте конкурентные очереди с .barrier для записи
  • Проверяйте текущую очередь перед синхронными вызовами:
extension DispatchQueue {
    func safeSync<T>(execute work: () throws -> T) rethrows -> T {
        if DispatchQueue.getLabel == self.label {
            // Уже на этой очереди, выполняем напрямую
            return try work()
        } else {
            return try sync(execute: work)
        }
    }
    
    private static var getLabel: String? {
        return String(validatingUTF8: __dispatch_queue_get_label(nil))
    }
}
  • Используйте асинхронные вызовы (async) как основной механизм внутри серийных очередей
  • Для обновления UI всегда переходите на DispatchQueue.main — это тоже серийная очередь, но специальная для работы с пользовательским интерфейсом

Практические последствия нарушения

  1. Зависание приложения без краша (deadlock не генерирует исключение)
  2. Неочевидные баги при рефакторинге кода
  3. Проблемы с производительностью — поток блокируется навсегда
  4. Сложности с отладкой — stack trace покажет ожидание в очереди, но не причину

Ключевой принцип: Серийные очереди отлично подходят для защиты общих ресурсов от состояния гонки, но требуют аккуратности с синхронными операциями. Всегда анализируйте контекст выполнения перед вызовом sync. Если вам нужна рекурсия или повторный вход в очередь, рассмотрите использование реентерабельных блокировок или переход на конкурентные очереди с контролем через барьеры.