Каким будет значение переменной-счетчика после выполнения 100 корутин, каждая из которых 1000 раз увеличит её на единицу?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Анализ проблемы
Классический вопрос о многопоточности (concurrency) и состоянии гонки (race condition). Корректный ответ зависит от контекста реализации, но в подавляющем большинстве случаев в Kotlin с корутинами и непотокобезопасным (non-thread-safe) счетчиком значение после выполнения будет меньше 100 000, и точное значение предсказать невозможно.
Основная причина: Race Condition (Состояние гонки)
Если 100 корутин запускаются параллельно в разных потоках (например, с Dispatchers.Default) и обращаются к общей изменяемой переменной без синхронизации, операции чтения и записи могут перекрываться.
import kotlinx.coroutines.*
import kotlin.system.measureTimeMillis
fun main() = runBlocking {
var counter = 0
val jobs = List(100) {
launch(Dispatchers.Default) {
repeat(1000) {
val temp = counter // 1. Чтение текущего значения
// Здесь может быть переключение контекста!
counter = temp + 1 // 2. Запись увеличенного значения
}
}
}
jobs.forEach { it.join() }
println("Counter = $counter")
}
Что происходит:
- Две корутины (А и Б) одновременно читают значение
counter, например,5. - Корутина А увеличивает своё значение до
6и записывает его. - Корутина Б уже прочитала
5, увеличивает до6и тоже записывает. - В итоге после двух операций инкремента счетчик равен
6, а не7.
Почему это происходит с корутинами?
- Корутины — легковесные, но не волшебные. При использовании многопоточного диспетчера (
Dispatchers.Default,Dispatchers.IOили собственного пула) корутины выполняются на реальных потоках. - Suspend-функции могут приостанавливаться в точках
suspend, но операцияcounter++— это не атомарная последовательность "чтение-изменение-запись" в контексте многопоточности. - Даже если использовать однопоточный диспетчер (например,
Dispatchers.MainилиnewSingleThreadContext), то гонки не будет, и результат будет ровно 100 000. Но в вопросе явно подразумевается параллельное выполнение.
Как получить предсказуемый результат (100 000)?
Для этого нужно использовать потокобезопасные (thread-safe) конструкции.
Вариант 1: Atomic
import java.util.concurrent.atomic.AtomicInteger
val atomicCounter = AtomicInteger(0)
fun main() = runBlocking {
val jobs = List(100) {
launch(Dispatchers.Default) {
repeat(1000) {
atomicCounter.incrementAndGet() // Атомарная операция
}
}
}
jobs.forEach { it.join() }
println("Counter = ${atomicCounter.get()}") // Гарантировано 100000
}
Вариант 2: Синхронизация (Mutex)
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
val mutex = Mutex()
var counter = 0
fun main() = runBlocking {
val jobs = List(100) {
launch(Dispatchers.Default) {
repeat(1000) {
mutex.withLock { // Только одна корутина работает с переменной
counter++
}
}
}
}
jobs.forEach { it.join() }
println("Counter = $counter") // Гарантировано 100000
}
Вариант 3: Ограничение диспетчером (если позволяет логика)
val singleThreadContext = newSingleThreadContext("MyThread")
var counter = 0
fun main() = runBlocking {
val jobs = List(100) {
launch(singleThreadContext) { // Все корутины в одном потоке
repeat(1000) {
counter++ // Гонки нет, так как нет параллелизма
}
}
}
jobs.forEach { it.join() }
println("Counter = $counter") // Гарантировано 100000
}
Итог
- Без синхронизации: Результат будет случайным числом меньше 100 000 (например, 95673, 98211 и т.д.). Чем больше корутин/потоков, тем сильнее расхождение.
- С синхронизацией или атомарными типами: Результат будет ровно 100 000.
Ключевой вывод для собеседования: Простое использование корутин не решает проблему синхронизации доступа к изменяемому состоянию (mutable state). Корутины упрощают асинхронный код, но для потокобезопасности по-прежнему требуются традиционные механизмы: атомарные переменные, мьютексы или проектирование без разделяемого изменяемого состояния (immutable state, акторы).