> Как реализовать потокобезопасный словарь (iOS, Swift)
Уровень: senior · Роль: mobile · Категория: Технические вопросы
Компании: Московская биржа
Стек: iOS, Swift
> Пример ответа
Короткий ответ
Потокобезопасный словарь в Swift реализуется через синхронизацию доступа к изменяемому хранилищу. Основные подходы: GCD-барьеры (DispatchQueue с .barrier для записи и sync для чтения), NSLock или os_unfair_lock, а также actor в современном Swift Concurrency. Для iOS предпочтителен actor - он даёт compile-time гарантии и автоматическую сериализацию. Для legacy-кода или высокой производительности - GCD с барьерами. Ключевое правило: никогда не возвращать ссылку на внутренний словарь напрямую.
Подробное объяснение
Потокобезопасность означает, что операции чтения и записи не приводят к гонкам данных (data races) и некорректному состоянию. Для словаря это критично, так как Dictionary - value type, но его мутация из нескольких потоков без синхронизации - undefined behavior.
Основные стратегии:
- GCD с барьером: чтение через
queue.sync, запись черезqueue.async(flags: .barrier). Барьер гарантирует, что запись выполняется эксклюзивно, а чтения - конкурентно. - NSLock / NSRecursiveLock: простой мьютекс, но блокирует все операции, включая чтения, что снижает производительность.
- Actor: изолирует состояние, компилятор запрещает прямой доступ извне. Все методы actor'а выполняются последовательно на его executor'е.
- os_unfair_lock: низкоуровневый, быстрый, но требует ручного управления и не поддерживает рекурсию.
Выбор зависит от контекста: для Swift 5.5+ и новых проектов - actor. Для обратной совместимости или тонкой настройки производительности - GCD.
На практике
В iOS-разработке чаще всего используется actor, так как он интегрируется с SwiftUI и Combine. Для кэшей, хранилищ настроек или in-memory баз данных - actor оптимален. Если нужен очень высокий конкурентный read-доступ и редкие записи - GCD с барьером. NSLock уместен в простых случаях, когда нет требований к производительности.
Важно: не использовать @unchecked Sendable без необходимости, а также не смешивать синхронные и асинхронные вызовы внутри actor без явного await.
Пример кода
SWIFT// Actor-basedactor ThreadSafeDictionary<Key: Hashable, Value> {private var storage: [Key: Value] = [:]func value(forKey key: Key) -> Value? {storage[key]}func set(_ value: Value, forKey key: Key) {storage[key] = value}func removeValue(forKey key: Key) -> Value? {storage.removeValue(forKey: key)}}// GCD-basedfinal class ThreadSafeDictionaryGCD<Key: Hashable, Value> {private var storage: [Key: Value] = [:]private let queue = DispatchQueue(label: "sync.dict", attributes: .concurrent)func value(forKey key: Key) -> Value? {queue.sync { storage[key] }}func set(_ value: Value, forKey key: Key) {queue.async(flags: .barrier) { self.storage[key] = value }}}
Как отвечать на собеседовании
Начни с краткого определения гонки данных. Затем перечисли три основных подхода, объясни trade-off каждого. Для senior-позиции важно упомянуть: actor - это не просто синхронизация, а модель изоляции, и что он не гарантирует отсутствие deadlock'ов при неправильном использовании await. Приведи пример, где GCD с барьером будет быстрее actor (много чтений, редкие записи). Подчеркни, что возврат storage напрямую из метода - антипаттерн, так как это копия value type, но если вернуть замыкание, работающее с внутренним состоянием - это утечка.
Что проверяет интервьюер
- Понимание модели памяти Swift и Sendable.
- Умение выбирать инструмент под задачу, а не использовать один шаблон.
- Знание ограничений actor (reentrancy, priority inversion).
- Практический опыт: как тестировать потокобезопасность (Thread Sanitizer, stress-тесты).
- Понимание разницы между синхронным и асинхронным доступом.
Типичные ошибки
- Использование
NSLockдля всех операций без необходимости - убивает конкурентность. - Возврат внутреннего словаря из метода - даже если это value type, копия создаётся, но если метод возвращает
[Key: Value], это безопасно, а если возвращаетDictionaryкакinout- нет. - Забыть про
barrierпри записи - тогда запись не эксклюзивна. - Использование actor для простого кэша без
async- вызовы становятся асинхронными, что усложняет код. - Не учитывать, что actor reentrant: внутри actor можно вызывать
awaitна другом actor'е, и состояние может измениться между await'ами.
> Похожие задачи по mobile
По какому времени работает функция reduce у массива
Все ли нормально с рекурсивно ссылающейся на себя структурой
Что должна возвращать функция сравнения
Что такое capture list в closure и как с ним работать
> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?
Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью