Какие способы синхронизации использовать для потокобезопасности
Что проверяют
Знаете ли вы инструменты от GCD до Swift Concurrency и понимаете ли, от чего каждый защищает. Следом почти всегда спрашивают про дедлок и про то, как Swift 6 ловит гонки на этапе компиляции.
Как отвечать
- последовательная очередь:
DispatchQueue(label:)без атрибутов, чтение черезsync, запись черезasync. Все обращения к состоянию идут строго по одному; - concurrent-очередь с барьером: чтения параллельно через
sync, запись черезasync(flags: .barrier). Барьер работает только на своей очереди, на глобальной он игнорируется; - блокировки:
NSLock,NSRecursiveLock, быстрыйos_unfair_lock. В Swift его безопасно использовать черезOSAllocatedUnfairLock(iOS 16+), а с iOS 18 естьMutexиз модуляSynchronization; - атомики:
Atomicиз того жеSynchronizationили пакет swift-atomics для счётчиков и флагов; - actor: изолирует изменяемое состояние, доступ снаружи через
await,@MainActorдля интерфейса. Swift 6 language mode проверяетSendableи изоляцию, и часть гонок становится ошибкой компиляции; DispatchSemaphore: для ограничения числа параллельных операций, а не как замена мьютексу.
Выбор: новый код на Swift Concurrency строят вокруг акторов, в синхронном коде короткую критическую секцию закрывают Mutex или OSAllocatedUnfairLock. Проверять решение стоит Thread Sanitizer в схеме Xcode.
Где спотыкаются
Считают атомарное свойство потокобезопасностью. В Swift модификатора atomic нет, @objc dynamic нужен для KVO и ничего не синхронизирует, а даже atomic в Objective-C защищает только отдельные чтение и запись: count += 1 остаётся гонкой.
Ловят дедлок: DispatchQueue.main.sync из главного потока или sync на ту же последовательную очередь изнутри её задачи.
Смешивают блокировки с async-кодом. Лок нельзя держать через await, а semaphore.wait() в асинхронной функции блокирует поток кооперативного пула, которых примерно столько же, сколько ядер. К тому же акторы реентерабельны: после await состояние актора могло измениться, и проверки, сделанные до await, надо повторять.