Как реализовать потокобезопасный массив в Swift?

Ответ

Потокобезопасный массив гарантирует корректность данных при конкурентном доступе из нескольких потоков. В Swift для этого можно использовать GCD (Grand Central Dispatch) с барьерными операциями.

Основной подход: Использовать DispatchQueue с атрибутом .concurrent.

  • Запись выполняется асинхронно с флагом .barrier, что делает её эксклюзивной.
  • Чтение выполняется синхронно для получения актуальных данных.
class ThreadSafeArray<Element> {
    private var array: [Element] = []
    private let queue = DispatchQueue(label: "threadSafe.array.queue", attributes: .concurrent)

    func append(_ element: Element) {
        queue.async(flags: .barrier) {
            self.array.append(element)
        }
    }

    var elements: [Element] {
        return queue.sync {
            return self.array
        }
    }
}

Почему это работает? Барьерная задача (flags: .barrier) ждёт завершения всех текущих задач в очереди и выполняется в одиночку, блокируя другие задачи на время записи. Это обеспечивает безопасность записи. Синхронное чтение (queue.sync) гарантирует, что возвращаемые данные будут соответствовать состоянию на момент вызова.

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

  • Для сценариев с интенсивной записью или более тонкого контроля можно использовать низкоуровневые примитивы: NSLock, os_unfair_lock или pthread_rwlock_t.
  • Реализация выше оптимизирована для частого чтения и редкой записи. Для полного набора операций (удаление, индексация) потребуется добавить аналогичные потокобезопасные методы.

Ответ 18+ 🔞

А, ну ты глянь, классика жанра — потокобезопасный массив! Это ж когда несколько потоков лезут в одну кучу данных, как пьяные в подъезд, и надо, чтобы они друг другу мозги не вынесли и не начитались хуйни вместо нужных данных.

Вот смотри, в Swift за это обычно отвечает GCD, этакая палочка-выручалочка для распиздяйства между потоками. Основная фишка — использовать очередь (DispatchQueue), но не простую, а конкурентную (.concurrent).

Суть проста, как три копейки:

  • Когда пишешь — делаешь это асинхронно, но с волшебным флажком .barrier. Это как крикнуть: «Все нахуй с дороги, я сейчас один буду делать!». Пока эта операция не закончится, все остальные потоки стоят и курят в сторонке.
  • Когда читаешь — делаешь это синхронно (sync). Это чтобы сразу, на месте, получить актуальные данные, а не какую-то левую версию из прошлой жизни массива.

Вот, смотри, как это выглядит в коде, блядь:

class ThreadSafeArray<Element> {
    private var array: [Element] = []
    private let queue = DispatchQueue(label: "threadSafe.array.queue", attributes: .concurrent)

    func append(_ element: Element) {
        queue.async(flags: .barrier) {
            self.array.append(element)
        }
    }

    var elements: [Element] {
        return queue.sync {
            return self.array
        }
    }
}

А почему, спрашивается, это работает? Всё гениально и просто, ёпта! Барьерная задача — это такой увалень, который говорит: «Я подожду, пока все эти мелкие читающие потоки свои дела закончат, а потом буду один, как царь, писать. И пока я пишу — все молчат, блядь!». Это и даёт безопасность записи. А синхронное чтение — это просто чтобы не улететь в будущее или прошлое, а получить данные именно сейчас, какие они есть.

Но есть нюансы, конечно, куда ж без них:

  • Если у тебя там адская писанина, а не чтение, то вся эта барьерная магия может начать тормозить, как старая кобыла. Тогда уже надо лезть в дебри с низкоуровневыми примитивами: NSLock, os_unfair_lock или вот эту страшную штуку pthread_rwlock_t. Это уже для настоящих гурманов боли.
  • Реализация выше — она для типичного случая «часто читаем, редко пишем». Если тебе надо удалять, искать по индексу или ещё какую дичь творить — придётся каждый метод оборачивать в эту же защитную обёртку, иначе будет пиздец и рассинхрон.

Короче, идея в том, чтобы организовать эту очередь, а не позволять потокам драться за массив, как собаки за кость.