Можно ли решить проблему data race с помощью serial очереди
Что проверяют
Понимаете ли вы, что синхронизация работает только тогда, когда через неё проходят все обращения. Ответ «да, serial очередь решает» верен лишь наполовину, и интервьюер обычно сразу дожимает: а что будет, если один поток читает переменную напрямую, и чем data race отличается от race condition.
Как отвечать
Да, решает, при одном условии: каждое чтение и каждая запись общего состояния идут через одну и ту же очередь. Дальше конкретика:
- запись:
queue.async { self.value += 1 }, вызывающая сторона не ждёт завершения; - чтение:
queue.sync { self.value }, потому что значение нужно вернуть здесь и сейчас; - deadlock:
syncиз той же очереди это гарантированная взаимная блокировка, поэтому обёртки не должны вызывать друг друга; - barrier: на собственной concurrent очереди
queue.async(flags: .barrier)даёт схему «много читателей, один писатель», при которой чтения идут параллельно; - альтернативы:
NSLock,OSAllocatedUnfairLock,Mutexиз модуляSynchronization.
Отдельно проговорите разницу между data race и race condition. Очередь убирает первое, но не второе: последовательность «проверил, что элемента нет, потом добавил» остаётся гонкой, если проверка и вставка это два отдельных обращения к очереди. Атомарной должна быть операция целиком.
Современный путь: в Swift Concurrency ту же задачу решает актор, а @MainActor привязывает состояние к главному потоку. В языковом режиме Swift 6 компилятор проверяет изоляцию статически и требует Sendable у типов, пересекающих границы. Реальные гонки в существующем коде ловит Thread Sanitizer.
Где спотыкаются
Заворачивают в очередь только запись, оставляя чтение прямым. Гонка никуда не делась, но проявляться начинает редко и плавающе, что хуже стабильного падения.
Делают одну глобальную очередь на всё приложение. Она превращается в узкое место: одна долгая операция внутри останавливает все остальные обращения к любому состоянию.
Путают очередь и поток. Serial очередь гарантирует порядок выполнения задач, но не гарантирует, что их выполнит один и тот же поток, поэтому thread-local хранилище и рекурсивные захваты ведут себя не так, как ожидается.