Как избежать race condition при работе с массивом в многопоточности
Что проверяют
Понимаете ли вы, почему Array в Swift не потокобезопасен, несмотря на семантику значения, и умеете ли подобрать способ синхронизации под задачу. Следом часто просят написать потокобезопасную обёртку или спрашивают, как то же самое решается в Swift Concurrency.
Как отвечать
Начните с причины: Array это структура с copy-on-write, но переменная, к которой обращаются несколько потоков, остаётся общим изменяемым состоянием. Одновременные append могут испортить буфер и уронить приложение, а не просто потерять элемент. Такие места ловит Thread Sanitizer в Xcode.
Способы защиты:
- serial
DispatchQueue: все чтения и записи идут через одну последовательную очередь, чтение черезsync, запись черезasync; - concurrent-очередь с барьером: чтения выполняются параллельно, запись через
async(flags: .barrier)ждёт их окончания и идёт одна. Схема «много читателей, один писатель»; - блокировки:
NSLock,OSAllocatedUnfairLock(iOS 16+),Mutexиз модуляSynchronization(iOS 18+). Критическая секция должна быть короткой; actor: массив прячется внутрь актора, снаружи доступ только черезawait. В Swift 6 со строгой проверкой конкурентности обращение в обход изоляции не скомпилируется;- не делить состояние: каждая задача собирает свою часть, результат склеивается в одном месте, например в
withTaskGroup.
Отдельно упомяните составные операции. Проверка if !items.isEmpty { items.removeFirst() } должна быть атомарной целиком, иначе гонка остаётся, даже если каждый метод обёртки защищён.
Где спотыкаются
Считают, что раз Array value type, гонок быть не может. Копия появляется только при присваивании, а свойство класса или глобальная переменная общие для всех потоков.
Защищают запись, но читают без синхронизации. Чтение во время перевыделения буфера так же опасно, как параллельная запись.
Вызывают sync на той же serial-очереди изнутри неё или держат блокировку во время сетевого запроса. Первое даёт дедлок или падение, второе превращает многопоточный код в последовательный.