Сделайте общий счётчик безопасным, когда тысяча корутин инкрементирует его конкурентно
Тысяча корутин на Dispatchers.Default инкрементирует один общий счётчик. Итоговое значение почти никогда не равно 1000.
Ограничения: корутины должны остаться конкурентными (не сводите их все к одному потоку), нельзя блокировать поток пула на время ожидания, а результат обязан быть ровно 1000.
class Counter {
private var count = 0
suspend fun incrementAll(): Int = coroutineScope {
repeat(1000) {
launch(Dispatchers.Default) {
// ваш код здесь
}
}
count
}
}
Допишите реализацию.
count++ — это чтение-изменение-запись, поэтому конкурентные корутины затирают обновления друг друга. Защитите его через mutex.withLock { count++ }: захват приостанавливает корутину, а не блокирует поток пула. AtomicInteger — безлоковая и более дешёвая альтернатива.
- ✗Думать, что
@Volatileделаетcount++атомарной операцией - ✗Считать, что инкремент
Intна JVM атомарен - ✗Удерживать блокирующий лок
synchronizedчерез точку приостановки
- →Когда
AtomicIntegerздесь предпочтительнееMutex? - →Как
limitedParallelism(1)решает ту же гонку вообще без лока?
Почему теряются инкременты
count++ компилируется в три шага: прочитать, прибавить, записать. Тысяча корутин на пуле Dispatchers.Default выполняет их вперемешку, и два потока, прочитавшие одно и то же значение, запишут одно и то же — один инкремент пропал.
Решение через Mutex
class Counter {
private val mutex = Mutex()
private var count = 0
suspend fun incrementAll(): Int = coroutineScope {
repeat(1000) {
launch(Dispatchers.Default) {
mutex.withLock { count++ }
}
}
count
}
}
withLock приостанавливает корутину, если лок занят, — поток пула при этом освобождается и берёт другую работу. coroutineScope дожидается всех детей, поэтому count читается уже после них.
Безлоковая альтернатива
class Counter {
private val count = AtomicInteger(0)
suspend fun incrementAll(): Int = coroutineScope {
repeat(1000) {
launch(Dispatchers.Default) { count.incrementAndGet() }
}
count.get()
}
}
Для одного счётчика это дешевле: incrementAndGet() — одна атомарная инструкция, ожидания нет вовсе. Mutex выигрывает, когда под защитой несколько связанных полей.