Урок по треку Swift и iOS 7 мин

GCD и OperationQueue

Очереди и QoS, sync и async, deadlock, барьеры, DispatchGroup, семафоры, Operation.

Даже в эпоху async/await GCD спрашивают почти всегда: огромное количество кода написано на нём, а задачи «что выведет» и «где deadlock» это любимый формат интервьюеров.

Очереди

  • Последовательная (serial): выполняет блоки по одному в порядке добавления. Своя serial-очередь работает на фоновых потоках.
  • Параллельная (concurrent): стартует блоки по порядку, но может выполнять несколько одновременно.
  • Главная очередь (DispatchQueue.main) это serial-очередь, которую обслуживает главный поток. Весь UI только здесь.
  • Глобальные очереди (DispatchQueue.global(qos:)) это системные concurrent-очереди.
let serial = DispatchQueue(label: "com.app.storage")
let concurrent = DispatchQueue(label: "com.app.cache", attributes: .concurrent)

sync и async

  • async: поставить блок и сразу продолжить.
  • sync: поставить блок и ждать, пока он выполнится.
let q = DispatchQueue(label: "serial")
print("1")
q.async { print("2") }
q.sync { print("3") }   // ждёт "2", потом выполняет "3"
print("4")
// 1 2 3 4

Deadlock

Главное правило: никогда не делайте sync на ту же serial-очередь, в которой находитесь.

// на главном потоке:
DispatchQueue.main.sync { }   // deadlock: главный поток ждёт сам себя

q.async {
    q.sync { }                // deadlock внутри своей же очереди
}

Другие сценарии: две очереди делают sync друг на друга; group.wait() на главном потоке, когда completion тоже ждёт главную очередь. В отладке помогает dispatchPrecondition(condition: .onQueue(q)).

QoS

QoSКогда
.userInteractiveанимации, обработка касаний, в пределах кадра
.userInitiatedпользователь нажал и ждёт результат
.utilityдолгая работа с прогрессом, загрузки
.backgroundневидимое: бэкап, индексация

DispatchGroup

Дождаться нескольких асинхронных операций:

let group = DispatchGroup()
for url in urls {
    group.enter()
    load(url) { _ in group.leave() }   // leave на всех путях!
}
group.notify(queue: .main) { self.reload() }

notify не блокирует поток, а wait() блокирует, на главном потоке так нельзя.

Барьеры: reader-writer

final class SafeCache {
    private var storage: [String: Data] = [:]
    private let queue = DispatchQueue(label: "cache", attributes: .concurrent)

    func get(_ key: String) -> Data? { queue.sync { storage[key] } }
    func set(_ key: String, _ value: Data) {
        queue.async(flags: .barrier) { self.storage[key] = value }
    }
}

Чтения идут параллельно, запись эксклюзивна. Барьер работает только на своей concurrent-очереди, на глобальных он игнорируется.

Семафор

DispatchSemaphore(value: 3) ограничивает число одновременных доступов. Не используйте wait() на главном потоке и в async-коде. Семафор не знает, кто его освободит, поэтому подвержен инверсии приоритетов. Для взаимного исключения лучше os_unfair_lock, OSAllocatedUnfairLock (iOS 16) или Mutex (iOS 18, Swift 6).

DispatchWorkItem

Обёртка над блоком, которую можно отменить до старта. Ручной debounce:

searchItem?.cancel()
let item = DispatchWorkItem { [weak self] in self?.search(text) }
searchItem = item
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: item)

Thread explosion

Если сотни блоков в глобальной очереди блокируются (синхронная сеть, sleep, семафор), GCD создаёт новые потоки взамен заблокированных: растут память и переключения контекста, вплоть до исчерпания лимита и зависания. Лечится ограничением параллельности и неблокирующими API.

OperationQueue

Надстройка над GCD с объектами-операциями:

let queue = OperationQueue()
queue.maxConcurrentOperationCount = 3

let auth = BlockOperation { refreshToken() }
let upload = BlockOperation { uploadFiles() }
upload.addDependency(auth)
queue.addOperations([auth, upload], waitUntilFinished: false)

Что даёт: зависимости, отмена (cancelAllOperations), лимит параллельности, состояние и KVO. Асинхронная операция (например, сетевой запрос внутри) требует переопределить start(), isAsynchronous и вручную менять isExecuting/isFinished с KVO, иначе операция «завершится» при выходе из main().

GCD или Swift Concurrency

Новый код пишут на async/await: компилятор проверяет гонки, отмена встроена, нет взрыва потоков. GCD остаётся в старом коде, в API, требующих очередь, и в низкоуровневых вещах (DispatchSource). Смешивать можно, но нельзя блокировать потоки кооперативного пула.

Что спросят на собесе

  • Разница serial и concurrent, sync и async.
  • Что выведет код с async и sync на одной очереди?
  • Где будет deadlock и почему?
  • Как дождаться нескольких запросов, как сделать потокобезопасный кеш?
  • Когда OperationQueue, а когда GCD?

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.