Как избежать гонки потоков
Что проверяют
Понимаете ли вы, откуда берётся гонка: два потока читают и пишут общее изменяемое состояние без согласования, и результат зависит от порядка. На Android следом почти всегда спрашивают, как это решается в корутинах и почему @Volatile не спасает счётчик.
Как отвечать
Начните с причины: count++ это три шага (прочитать, прибавить, записать), и два потока легко теряют обновление. Дальше способы, от лучшего к вынужденному:
- не делить состояние: неизменяемые модели,
data classсcopy(), данные передаются между потоками, а не правятся на месте; - ограничить одним потоком: состояние экрана меняется только на
Dispatchers.Main, а для фоновой работы подойдёт последовательный диспетчерDispatchers.Default.limitedParallelism(1); - атомарные операции:
AtomicInteger,AtomicReference, уMutableStateFlowфункцияupdate { }, которая делает compare-and-set в цикле; Mutexиз kotlinx.coroutines:mutex.withLock { }приостанавливает корутину, а не блокирует поток;synchronizedиReentrantLock: для обычного кода вне корутин, критическая секция короткая и без ввода-вывода;- потокобезопасные коллекции:
ConcurrentHashMapвместоHashMapс ручной синхронизацией.
Про @Volatile скажите точно: он даёт видимость записи другим потокам и запрещает переупорядочивание, но не делает составную операцию атомарной. Для флага «остановиться» подходит, для счётчика нет.
Хорошо добавить, как гонку ловят: стресс-тест с тысячами корутин на Dispatchers.Default и проверкой итога, а на ревью поиск общих var, которые меняются из разных мест.
Где спотыкаются
Защищают только запись, а чтение оставляют без синхронизации. Геттер без synchronized может вернуть устаревшее значение, модель памяти Java тут ничего не обещает.
Считают, что Dispatchers.IO решает проблему. Это пул потоков, и две корутины на нём выполняются параллельно, гонка остаётся.
Не знают, что Mutex в корутинах нереентерабельный. Вложенный withLock на том же мьютексе не упадёт с ошибкой, а просто зависнет навсегда.