Как избежать гонок данных при запуске множества корутин?
Что проверяют
Понимаете ли вы, что корутины не отменяют многопоточность: на Dispatchers.Default и Dispatchers.IO тысяча launch выполняется на пуле потоков, и counter++ из них теряет инкременты. Следом обычно спрашивают, чем Mutex отличается от synchronized и почему state.value++ у MutableStateFlow тоже небезопасен.
Как отвечать
Начните с главного: лучший способ избежать гонки это не иметь общего изменяемого состояния. Дальше инструменты, от простого к сложному:
- ограничение одним потоком: состояние меняется только на
Dispatchers.Main(вviewModelScopeтак и есть) или наDispatchers.Default.limitedParallelism(1). Вычисления идут параллельно, а запись последовательно; - атомики:
AtomicInteger,AtomicReferenceдля одиночных счётчиков и флагов, без приостановки и блокировок; MutableStateFlow.update { }: атомарное обновление через compare-and-set. В записиstate.value = state.value.copy(...)между чтением и записью может вклиниться другая корутина;MutexиwithLock: критическая секция, внутри которой можно вызывать suspend-функции. Ожидающая корутина приостанавливается и не держит поток;Channelс одним потребителем: корутины шлют команды, а одна корутина владеет состоянием и применяет их по очереди. Это модель актора без билдераactor, который помечен как obsolete.
Если нужно не исключить параллельный доступ, а ограничить его, назовите Semaphore из kotlinx.coroutines.sync: например, не больше четырёх одновременных загрузок.
Где спотыкаются
Оборачивают suspend-вызов в synchronized. Компилятор не пустит точку приостановки внутрь критической секции, а блокирующий замок на пуле Dispatchers.Default отнимает поток у остальных корутин.
Считают, что @Volatile решает проблему. Он даёт видимость записи другим потокам, но x++ остаётся тремя операциями: чтение, изменение, запись.
Забывают, что Mutex не реентерабельный. Повторный withLock из кода, который уже держит этот же замок, приводит к дедлоку, а не к повторному входу.