Что произойдет при одновременной записи в массив с разных потоков
Что проверяют
Знаете ли вы, что Array в Swift это значимый тип с копированием при записи, и понимаете ли, что гонка тут кончается не «неверным числом», а порчей памяти. Плюс умение изолировать состояние средствами платформы.
Как отвечать
Массив в Swift это структура, за которой стоит буфер в куче и механизм copy-on-write. Одновременный append из двух потоков ломает не логику, а сам буфер: оба потока видят буфер уникальным, одновременно правят счётчик и реаллоцируют память. Итог это потерянные элементы, двойное освобождение и падение с EXC_BAD_ACCESS далеко от места ошибки, а иногда молча испорченные данные. Формально это неопределённое поведение, а не «непредсказуемое значение».
Как закрывать доступ:
- последовательная очередь и весь доступ через неё: запись
async, чтениеsync; - барьер на параллельной очереди (
queue.async(flags: .barrier)) для схемы «много читателей, один писатель»; NSLockилиos_unfair_lock, если нужна минимальная стоимость;- актор: состояние изолировано по языку, снаружи доступ только через
await.
Хорошо добавить, что строгая проверка конкурентности ловит часть таких мест компилятором через Sendable, а искать уже случившееся нужно Thread Sanitizer: он показывает обе стороны гонки со стеками.
Где спотыкаются
Считают, что раз массив значимый тип, копии независимы и гонки быть не может. Независимы они только после записи, а до неё буфер общий, и сам механизм copy-on-write не потокобезопасен: проверка уникальности и копирование не одна атомарная операция.
Ставят sync там, где он приводит к взаимной блокировке: вызов queue.sync изнутри той же последовательной очереди вешает поток навсегда. Соседняя ошибка в примерах: пишут двумя асинхронными блоками, а результат печатают сразу, без ожидания, и получают половину данных, после чего делают неверный вывод о защите.
Называют DispatchQueue потокобезопасной структурой данных. Очередь это способ исполнения, а безопасность даёт дисциплина «любое обращение только через неё». Одна прямая запись мимо очереди обнуляет всю схему, и такие места не видны в обзоре кода, если обёртка отдаёт внутреннее хранилище наружу.