> Как реализовать потокобезопасный словарь (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-based
actor 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-based
final 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'ами.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью