Когда и зачем использовать DispatchQueue для синхронизации
Что проверяют
Понимаете ли вы, как очередь превращается в замок для общего состояния и чем это грозит: дедлоками и лишними переключениями потоков. Сейчас следом почти наверняка спросят, чем это лучше или хуже актора и Mutex в Swift 6.
Как отвечать
Если весь доступ к данным идёт через одну последовательную очередь, блоки выполняются по одному, и гонки нет.
- serial-очередь как замок: приватная
DispatchQueue(label:), чтение черезqueue.sync { value }, запись черезsyncилиasync. Через очередь проходят и чтения, и записи; - reader-writer: очередь с
attributes: .concurrent, чтения идут параллельно черезsync, запись черезasync(flags: .barrier). Барьер дожидается текущих блоков и выполняется в одиночку; - главная очередь: всё, что трогает UIKit, выполняется на
DispatchQueue.main, это тоже синхронизация, только по потоку.
Плюс очереди в том, что её невозможно забыть разблокировать, а владелец данных виден в коде. Минус в накладных расходах: блок в очереди дороже, чем захват os_unfair_lock.
Альтернативы, которые стоит назвать:
OSAllocatedUnfairLock(iOS 16) иMutexиз модуляSynchronization(iOS 18): быстрые замки для коротких критических секций с синхронным доступом;- акторы: основной способ в новом коде на Swift Concurrency. В строгом режиме Swift 6 компилятор сам проверяет изоляцию, но доступ к состоянию становится асинхронным, через
await.
Очереди остаются уместны в коде на GCD и там, где нужен синхронный интерфейс без переписывания на async.
Где спотыкаются
Вызывают sync на той очереди, где уже находятся. Самый частый случай: DispatchQueue.main.sync с главного потока, это мгновенный дедлок и падение.
Ставят барьер на глобальную очередь. На DispatchQueue.global() флаг .barrier не действует, блок выполняется как обычный, и защита пропадает.
Защищают только запись, а читают переменную напрямую. Гонка остаётся, и Thread Sanitizer её покажет.