Как реализовать потокобезопасный словарь
Что проверяют
Знаете ли вы инструменты синхронизации в Swift и понимаете ли, где кончается их гарантия. Главный уточняющий вопрос: безопасна ли операция «прочитать, изменить, записать», если чтение и запись по отдельности защищены.
Как отвечать
Нужна обёртка над [Key: Value] с приватным хранилищем и единственным путём доступа. Варианты:
- последовательная очередь:
DispatchQueueбез атрибутов, чтение черезsync, запись черезsyncилиasync. Просто и надёжно; - конкурентная очередь с барьером: чтения идут параллельно через
sync, запись черезasync(flags: .barrier). Схема «много читателей, один писатель», но очередь должна быть своей: на глобальных очередях барьер не действует; - блокировка:
NSLockилиOSAllocatedUnfairLock(iOS 16+), а при минимальной iOS 18Mutexиз модуляSynchronization. На коротких операциях со словарём это обычно быстрее очередей; actor: компилятор сам гарантирует изоляцию, но каждый вызов снаружи становитсяawait, и из синхронного кода им так просто не воспользуешься.
Сильный ответ добавляет атомарные составные операции: метод вроде mutate(_:), который выполняет всю логику «прочитать и изменить» внутри одного захвата. Для Swift 6 со строгой проверкой конкурентности класс должен быть Sendable: с Mutex это получается честно, с ручной блокировкой приходится писать @unchecked Sendable и отвечать за корректность самому.
Где спотыкаются
Защищают get и set по отдельности, а потом пишут dict[key, default: 0] += 1. Между чтением и записью влезает другой поток, и инкремент теряется. Тем же страдает популярная обёртка свойства @Atomic.
Вызывают queue.sync изнутри той же последовательной очереди, например из колбэка, и получают взаимную блокировку. С конкурентной очередью та же ловушка срабатывает, когда внутри барьера кто-то снова зовёт sync.
Хранят os_unfair_lock как обычное поле и передают его через &. Swift не гарантирует стабильный адрес такой переменной, поэтому блокировку берут в OSAllocatedUnfairLock или выделяют под неё память вручную.