Является ли стандартный словарь (Dictionary) в Swift потокобезопасным?

«Является ли стандартный словарь (Dictionary) в Swift потокобезопасным?» — вопрос из категории Многопоточность, который задают на 10% собеседований IOS Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Нет, стандартный Dictionary в Swift не является потокобезопасным. Одновременные операции чтения и, особенно, записи из разных потоков приводят к состоянию гонки (data race), что может вызвать неожиданные краши, повреждение данных или неконсистентное состояние коллекции.

Почему это происходит? Внутренняя структура словаря (хеш-таблица) не защищена от конкурентного доступа. Запись может изменять эту структуру (рехеширование), в то время как другой поток пытается читать.

Способы сделать Dictionary потокобезопасным:

  1. Использование DispatchQueue с барьером (barrier) для конкурентной очереди: Это эффективный способ, позволяющий множественное чтение параллельно, но блокирующий очередь на время записи.

    class ThreadSafeDictionary<Key: Hashable, Value> {
        private var dictionary = [Key: Value]()
        private let queue = DispatchQueue(label: "com.example.dictionaryQueue", attributes: .concurrent)
    
        func set(_ value: Value, for key: Key) {
            queue.async(flags: .barrier) { // Барьерная задача
                self.dictionary[key] = value
            }
        }
    
        func get(for key: Key) -> Value? {
            queue.sync { // Обычный синхронный вызов для чтения
                return self.dictionary[key]
            }
        }
    }
  2. Использование акторов (Actors) в Swift 5.5+: Акторы изолируют свое состояние, гарантируя, что к нему обращается только одна задача в данный момент.

    actor SafeDictionaryActor<Key: Hashable, Value> {
        private var storage = [Key: Value]()
    
        func set(_ value: Value, for key: Key) {
            storage[key] = value
        }
    
        func get(for key: Key) -> Value? {
            return storage[key]
        }
    }
    
    // Использование:
    let dictActor = SafeDictionaryActor<String, Int>()
    Task {
        await dictActor.set(42, for: "answer")
        let value = await dictActor.get(for: "answer")
    }
  3. Использование примитивов синхронизации (NSLock, pthread_rwlock_t): Более низкоуровневый, но иногда необходимый подход.

Альтернативы:

  • NSCache: Класс из Foundation, который частично потокобезопасен (не требует дополнительных блокировок для доступа), но автоматически удаляет объекты при нехватке памяти и не гарантирует сохранение всех элементов.
  • Специализированные библиотеки: Например, ConcurrentDictionary из сторонних пакетов.

Вывод: Всегда обеспечивайте синхронизацию при доступе к изменяемому Dictionary из нескольких потоков.