Как сделать потокобезопасным общий массив при синхронных операциях в concurrent очереди
Что проверяют
Понимаете ли вы разницу между «синхронно» и «последовательно». sync означает, что вызывающий поток ждёт, а параллельность задаёт тип очереди. Следом обычно просят написать reader-writer обёртку и спрашивают, как то же решить в Swift Concurrency.
Как отвечать
Начните с диагноза: queue.sync на concurrent-очереди не сериализует доступ. Если два потока одновременно зовут sync, блоки выполняются параллельно, и append из разных потоков портит массив или роняет приложение. Array в Swift это тип-значение с copy-on-write, но потокобезопасным от этого он не становится.
Классическое решение, reader-writer на своей concurrent-очереди:
- чтение:
queue.sync { array[index] }, несколько читателей работают параллельно; - запись:
queue.async(flags: .barrier) { array.append(x) }, барьерный блок ждёт завершения текущих чтений, выполняется в одиночку, а задачи после него ждут своей очереди; - составные операции: «проверить
countи взять элемент» делаются одним блоком, иначе между проверкой и доступом вклинится запись.
Барьер работает только на очереди, созданной вами с attributes: .concurrent. На глобальной очереди флаг .barrier ничего не делает.
Затем альтернативы. Последовательная очередь или NSLock проще и часто быстрее, если чтений не подавляющее большинство. OSAllocatedUnfairLock (iOS 16+) и Mutex из фреймворка Synchronization (iOS 18+) дают дешёвую блокировку. В Swift 6 со строгой проверкой конкурентности естественный вариант это actor: изоляцию проверяет компилятор, а обращение снаружи становится await.
Где спотыкаются
Делают sync и для чтения, и для записи на concurrent-очереди без барьера и считают, что всё защищено. Такую гонку сразу показывает Thread Sanitizer, стоит упомянуть, что вы его включаете.
Вызывают queue.sync изнутри блока на той же очереди. На последовательной очереди это мгновенный deadlock, на concurrent он случается, только когда между ними встаёт барьерная запись, поэтому ловится лишь под нагрузкой.
Защищают отдельные методы и забывают про последовательность вызовов: if arr.count > 0 { arr.get(0) } остаётся гонкой, даже если оба метода внутри под очередью.