Даже в эпоху 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?