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

Ответ

Нет, стандартный 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 из нескольких потоков.

Ответ 18+ 🔞

А, ну ты про словари в свифте, да? Потокобезопасность, блядь. Так вот, слушай сюда, распиздяй: стандартный Dictionary — это как голая жопа на морозе, если ты его из нескольких потоков трогаешь. Он тебе такую гонку данных устроит, что мало не покажется. Может и не сразу, но в самый неподходящий момент — пиздец, краш, и ты потом неделю дебажишь, почему у тебя значение nil, а должно быть 42, блядь.

Почему так, спросишь? Да потому что внутри он, сука, хеш-таблица, а её структуру при записи может перестраивать. Представь: один поток там рехеширует всё, перекладывает по новым квартирам, а другой в это же время пытается ключ найти. И всё, приехали. Состояние гонки, ёпта. Удивление пиздец.

Ну и как с этим бороться, ебать мои старые костыли? Вариантов — овердохуища.

  1. DispatchQueue с барьером — классика, блядь. Умные чтения делаются параллельно, а запись — одна за раз, как в хорошем туалете.

    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. Акторы (Swift 5.5+). Вот это уже красота, блядь. Сам язык говорит: «Отойди, мудила, я сам всё изолирую». Только через await к нему подходи.

    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). Для мазохистов, которым неймётся. Самый прямой путь, но и самый опасный — можно забыть разлочиться и всех заморозить, пидарас шерстяной.

А что насчёт NSCache? Ну, это хитрая жопа. Он вроде как потокобезопасный из коробки, но он может твои данные в любой момент выкинуть, если память кончится. Доверия ебать ноль, если нужно всё сохранить.

Короче, вывод, блядь, простой: не лезь к обычному словарю из разных потоков без защиты. Или оберни, или используй актор, или иди нахуй. Выбирай.