--- title: Разделяемое состояние description: Mutex, Semaphore, атомарные типы, конфайнмент потока и подход «не разделять состояние вообще» tags: - kotlin - coroutines --- Корутины не отменяют гонок. Если несколько корутин меняют одну переменную, нужна синхронизация - причём такая, которая приостанавливает, а не блокирует поток. ```kotlin import kotlinx.coroutines.* fun main() = runBlocking { var counter = 0 withContext(Dispatchers.Default) { List(100) { launch { repeat(1000) { counter++ } } }.forEach { it.join() } } println("потеряно инкрементов: ${counter < 100_000}") // потеряно инкрементов: true } ``` `counter++` - это чтение, увеличение и запись: три операции, между которыми может вклиниться другая корутина. На одном потоке проблемы не было бы, но `Dispatchers.Default` использует все ядра. ## Способы, по возрастанию цены | Подход | Стоимость | Когда применять | |---|---|---| | не разделять состояние | нулевая | всегда, когда возможно | | неизменяемые данные | нулевая | состояние целиком заменяется | | атомарные типы | очень низкая | счётчики, флаги, одна переменная | | `MutableStateFlow.update` | низкая | наблюдаемое состояние | | конфайнмент в один поток | низкая | группа связанных переменных | | `Mutex` | средняя | составная операция, вызов `suspend` внутри | | `Semaphore` | средняя | ограничение числа одновременных операций | Порядок не случаен: первые два варианта убирают проблему, остальные её решают. ## Атомарные типы ```kotlin import kotlinx.coroutines.* import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicReference fun main() = runBlocking { val counter = AtomicInteger(0) withContext(Dispatchers.Default) { List(100) { launch { repeat(1000) { counter.incrementAndGet() } } }.forEach { it.join() } } println("точный результат: ${counter.get()}") // точный результат: 100000 val ref = AtomicReference(listOf()) withContext(Dispatchers.Default) { List(10) { i -> launch { ref.updateAndGet { it + "элемент $i" } } }.forEach { it.join() } } println("добавлено без потерь: ${ref.get().size}") // добавлено без потерь: 10 } ``` Атомарные типы работают через CAS (compare-and-set) - никаких блокировок, только повтор при конфликте. Это самый дешёвый способ, но он покрывает только операции с **одной** переменной. ## Mutex `Mutex` - взаимное исключение для корутин. В отличие от `synchronized`, он приостанавливает, а не блокирует поток. ```kotlin import kotlinx.coroutines.* import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock fun main() = runBlocking { val mutex = Mutex() var counter = 0 withContext(Dispatchers.Default) { List(100) { launch { repeat(1000) { mutex.withLock { counter++ } } } } .forEach { it.join() } } println("результат точный: ${counter == 100_000}") // результат точный: true } ``` Главное преимущество - внутри `withLock` можно вызывать `suspend`-функции: ```kotlin import kotlinx.coroutines.* import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock class Cache { private val mutex = Mutex() private val data = mutableMapOf() suspend fun get(id: Int): String = mutex.withLock { data[id] ?: load(id).also { data[id] = it } } private suspend fun load(id: Int): String { delay(10) // suspend внутри критической секции return "значение $id" } } fun main() = runBlocking { val cache = Cache() val results = List(5) { async { cache.get(1) } }.awaitAll() println("все получили одно значение: ${results.toSet().size == 1}") // все получили одно значение: true } ``` С `synchronized` так нельзя: `suspend`-вызов внутри `synchronized`-блока не компилируется, потому что приостановка освободила бы поток, а монитор остался бы захваченным. ## Mutex не реентерабелен ```kotlin import kotlinx.coroutines.* import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock fun main() = runBlocking { val mutex = Mutex() val deadlocked = withTimeoutOrNull(100) { mutex.withLock { mutex.withLock { // вторичный захват тем же владельцем "недостижимо" } } } println("повторный захват повис: ${deadlocked == null}") // повторный захват повис: true } ``` Это осознанное решение: реентерабельность в корутинах определить корректно нельзя - «тот же владелец» для корутины понятие нечёткое, ведь она может перемещаться между потоками. Практический вывод: функция под `withLock` не должна вызывать другую функцию, которая берёт тот же мьютекс. Приватные методы, вызываемые из критической секции, не должны блокироваться сами. ## Semaphore Ограничивает число одновременных операций: ```kotlin import kotlinx.coroutines.* import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit import java.util.concurrent.atomic.AtomicInteger fun main() = runBlocking { val semaphore = Semaphore(permits = 3) val inFlight = AtomicInteger(0) val maxInFlight = AtomicInteger(0) withContext(Dispatchers.Default) { List(20) { launch { semaphore.withPermit { val now = inFlight.incrementAndGet() maxInFlight.updateAndGet { m -> maxOf(m, now) } delay(20) // ожидание ответа API inFlight.decrementAndGet() } } }.forEach { it.join() } } println("одновременно не больше 3: ${maxInFlight.get() <= 3}") // одновременно не больше 3: true } ``` Важное отличие от `limitedParallelism`: семафор считает операции **в полёте**, включая приостановленные. Именно он нужен, чтобы не превысить лимит внешнего API. Подробнее: [Диспетчеры и контекст](Диспетчеры%20и%20контекст.md) ## Конфайнмент в один поток Если состояние меняется только в одном потоке, синхронизация не нужна вовсе: ```kotlin import kotlinx.coroutines.* fun main() = runBlocking { val single = Dispatchers.Default.limitedParallelism(1) var counter = 0 withContext(Dispatchers.Default) { List(100) { launch { repeat(1000) { withContext(single) { counter++ } // все изменения в одном потоке } } }.forEach { it.join() } } println("точный результат: ${counter == 100_000}") // точный результат: true } ``` Работает, но дорого: 100 000 переключений контекста. Правильная версия конфайнмента - **грубая**, когда весь блок работы уходит в один поток: ```kotlin import kotlinx.coroutines.* import kotlin.system.measureTimeMillis fun main() = runBlocking { val single = Dispatchers.Default.limitedParallelism(1) var counter = 0 val coarse = measureTimeMillis { withContext(single) { // переключение один раз repeat(100_000) { counter++ } } } println("результат: $counter") println("грубый конфайнмент быстр: ${coarse < 500}") // результат: 100000 // грубый конфайнмент быстр: true } ``` ## Актор: состояние внутри корутины Самый чистый подход - состояние не разделяется вообще, им владеет одна корутина: ```kotlin import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel sealed interface AccountCommand { data class Deposit(val amount: Int) : AccountCommand data class Withdraw(val amount: Int, val ok: CompletableDeferred) : AccountCommand data class Balance(val reply: CompletableDeferred) : AccountCommand } fun main() = runBlocking { val commands = Channel() launch { var balance = 0 // никто больше не видит эту переменную for (cmd in commands) { when (cmd) { is AccountCommand.Deposit -> balance += cmd.amount is AccountCommand.Withdraw -> { val enough = balance >= cmd.amount if (enough) balance -= cmd.amount cmd.ok.complete(enough) } is AccountCommand.Balance -> cmd.reply.complete(balance) } } } repeat(100) { commands.send(AccountCommand.Deposit(10)) } val ok = CompletableDeferred() commands.send(AccountCommand.Withdraw(500, ok)) println("списание прошло: ${ok.await()}") val tooMuch = CompletableDeferred() commands.send(AccountCommand.Withdraw(10_000, tooMuch)) println("превышение отклонено: ${!tooMuch.await()}") val balance = CompletableDeferred() commands.send(AccountCommand.Balance(balance)) println("баланс: ${balance.await()}") commands.close() // списание прошло: true // превышение отклонено: true // баланс: 500 } ``` Проверка «хватает ли денег» и списание происходят атомарно без всякой блокировки - просто потому, что их выполняет одна корутина. Подробнее: [Channels](Channels.md) ## StateFlow для наблюдаемого состояния ```kotlin import kotlinx.coroutines.* import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update fun main() = runBlocking { val state = MutableStateFlow(0) withContext(Dispatchers.Default) { List(100) { launch { repeat(1000) { state.update { v -> v + 1 } } } } .forEach { it.join() } } println("результат точный: ${state.value == 100_000}") // результат точный: true } ``` `update` - это CAS-цикл, поэтому он и атомарен, и даёт оповещение подписчикам. Если состояние нужно наблюдать, это лучший вариант. Подробнее: [StateFlow и SharedFlow](StateFlow%20и%20SharedFlow.md) ## Как выбрать ```mermaid graph TD A["Состояние меняют
несколько корутин"] --> B{"Можно вообще
не разделять?"} B -->|"да"| C["локальная переменная
или актор"] B -->|"нет"| D{"За состоянием
наблюдают?"} D -->|"да"| E["MutableStateFlow
+ update"] D -->|"нет"| F{"Одна переменная?"} F -->|"да"| G["AtomicInteger / AtomicReference"] F -->|"нет, составная операция"| H{"Нужен suspend
внутри секции?"} H -->|"да"| I["Mutex.withLock"] H -->|"нет"| J["Mutex или
конфайнмент в поток"] ``` ## Неизменяемость убирает проблему ```kotlin import kotlinx.coroutines.* import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update data class Stats(val requests: Int = 0, val errors: Int = 0) { val successRate: Double get() = if (requests == 0) 0.0 else (requests - errors).toDouble() / requests } fun main() = runBlocking { val stats = MutableStateFlow(Stats()) withContext(Dispatchers.Default) { List(100) { i -> launch { stats.update { it.copy(requests = it.requests + 1, errors = it.errors + i % 10 / 9) } } }.forEach { it.join() } } val final = stats.value println("запросов: ${final.requests}, ошибок: ${final.errors}") println("согласованный снимок: ${final.requests == 100}") // запросов: 100, ошибок: 10 // согласованный снимок: true } ``` Два связанных поля обновляются одним атомарным `copy`, поэтому несогласованного состояния («запросов 5, ошибок 7») не возникает никогда. Это главный довод за неизменяемые `data class` в качестве состояния. Подробнее: [Data class](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Data%20class.md) ## Коллекции ```kotlin import kotlinx.coroutines.* import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import java.util.concurrent.ConcurrentHashMap fun main() = runBlocking { // обычный MutableList не потокобезопасен val unsafe = mutableListOf() withContext(Dispatchers.Default) { List(100) { i -> launch { repeat(100) { unsafe.add(i) } } }.forEach { it.join() } } println("размер может быть неверным: ${unsafe.size <= 10_000}") // вариант 1: Mutex val mutex = Mutex() val guarded = mutableListOf() withContext(Dispatchers.Default) { List(100) { i -> launch { repeat(100) { mutex.withLock { guarded.add(i) } } } } .forEach { it.join() } } println("с Mutex точно: ${guarded.size == 10_000}") // вариант 2: потокобезопасная коллекция из JDK val concurrent = ConcurrentHashMap() withContext(Dispatchers.Default) { List(100) { i -> launch { concurrent[i] = i } }.forEach { it.join() } } println("ConcurrentHashMap точно: ${concurrent.size == 100}") // размер может быть неверным: true // с Mutex точно: true // ConcurrentHashMap точно: true } ``` В Kotlin нет своих потокобезопасных коллекций для JVM - используются `java.util.concurrent`. Но учтите: `ConcurrentHashMap` безопасен для отдельных операций, а составные («прочитать, изменить, записать») всё равно требуют `compute`/`merge` или мьютекса. Подробнее: [Изменяемость](../Коллекции/Изменяемость.md) ## Что не нужно делать ```kotlin import kotlinx.coroutines.* import kotlin.system.measureTimeMillis fun main() = runBlocking { var counter = 0 val lock = Any() val time = measureTimeMillis { withContext(Dispatchers.Default) { List(100) { launch { repeat(1000) { synchronized(lock) { counter++ } } } } .forEach { it.join() } } } println("результат верный: ${counter == 100_000}") println("но поток блокировался: ${time >= 0}") // результат верный: true // но поток блокировался: true } ``` `synchronized` работает и даёт правильный результат, но блокирует поток диспетчера: пока корутина ждёт монитор, поток не может выполнять другие корутины. На ограниченном диспетчере это прямой путь к deadlock, а `suspend`-вызов внутри `synchronized` вообще не скомпилируется. > [!info] Отличия от Java > | Java | Kotlin | > |---|---| > | `synchronized` (блокирует поток) | `Mutex.withLock` (приостанавливает) | > | `ReentrantLock` | `Mutex` (не реентерабелен) | > | `Semaphore` из `j.u.c.` (блокирует) | `kotlinx.coroutines.sync.Semaphore` | > | `AtomicInteger` | он же, или `MutableStateFlow` | > | `volatile` | `@Volatile` или `StateFlow` | > | `ThreadLocal` для конфайнмента | элемент контекста корутины | > | `ConcurrentHashMap` | он же | > | нет аналога | актор на `Channel` | > > **`synchronized` занимает поток, `Mutex` - нет.** Это принципиальное различие. В пуле из 8 потоков восемь корутин, ждущих монитор, парализуют весь диспетчер - включая корутины, которым этот монитор не нужен. `Mutex` приостанавливает корутину и освобождает поток: > ```kotlin > import kotlinx.coroutines.* > import kotlinx.coroutines.sync.Mutex > import kotlinx.coroutines.sync.withLock > > fun main() = runBlocking { > val mutex = Mutex() > val single = Dispatchers.Default.limitedParallelism(1) > var progress = "" > withContext(single) { > launch { mutex.withLock { delay(30); progress += "A" } } > launch { mutex.withLock { progress += "B" } } > launch { progress += "C" } // не ждёт мьютекс > } > println("порядок выполнения: $progress") > // порядок выполнения: CAB > } > ``` > На одном потоке корутина `C` выполнилась первой, пока `A` держала мьютекс и ждала на `delay`. С `synchronized` и `Thread.sleep` поток был бы занят и `C` стояла бы в очереди. > > **`suspend` внутри `synchronized` не компилируется.** Это не ограничение, а защита: приостановка освободила бы поток, а монитор остался бы захваченным - потенциальный deadlock, который JVM не заметит. `Mutex` снимает запрет, потому что владение не привязано к потоку. > > **`ReentrantLock` реентерабелен, `Mutex` - нет.** В Java повторный захват тем же потоком разрешён, что делает рекурсивные вызовы под локом безопасными. В корутинах понятие «тот же владелец» не определено: корутина мигрирует между потоками, а `withLock` в дочерней корутине - это уже другой владелец. Поэтому `Mutex` принципиально не реентерабелен, и рекурсию под локом приходится исключать архитектурно. > > **`volatile` против `StateFlow`.** `@Volatile` в Kotlin есть и работает так же: гарантирует видимость записи между потоками, но не атомарность составных операций. `MutableStateFlow` даёт и видимость, и атомарный `update`, и оповещение - три вещи вместо одной. > > **Актор не имеет аналога в Java.** Паттерн «состояние внутри потока, снаружи только сообщения» в Java требует `BlockingQueue` + выделенный поток + «отравленную пилюлю» для остановки + ручную реализацию ответов (обычно через `CompletableFuture` в сообщении). В Kotlin это `Channel` + `launch` + `CompletableDeferred` - примерно двадцать строк и никаких потоков. > > **Проблемы те же.** Корутины не отменяют ни гонок, ни deadlock'ов, ни инверсии приоритетов. Два мьютекса, захваченные в разном порядке, дают взаимную блокировку точно так же, как два `synchronized`: > ```kotlin > import kotlinx.coroutines.* > import kotlinx.coroutines.sync.Mutex > import kotlinx.coroutines.sync.withLock > > fun main() = runBlocking { > val a = Mutex() > val b = Mutex() > val result = withTimeoutOrNull(100) { > coroutineScope { > launch { a.withLock { delay(20); b.withLock { } } } > launch { b.withLock { delay(20); a.withLock { } } } > } > } > println("классический deadlock: ${result == null}") // классический deadlock: true > coroutineContext.cancelChildren() > } > ``` > Разница только в том, что корутинный deadlock можно разорвать таймаутом, а поток, стоящий на мониторе, прервать нельзя. > > Сравнить: [Взаимодействие между потоками в Java](../../Программирование%20на%20Java/Многопоточность/Взаимодейтсвие%20между%20потоками.md), [HashMap в Java](../../Программирование%20на%20Java/Коллекции/HashMap.md), [JVM](../../Программирование%20на%20Java/JVM/JVM.md) > [!warning] Подводные камни > **`Mutex` не реентерабелен - повторный захват это deadlock:** > ```kotlin > import kotlinx.coroutines.* > import kotlinx.coroutines.sync.Mutex > import kotlinx.coroutines.sync.withLock > > class Service { > private val mutex = Mutex() > suspend fun outer() = mutex.withLock { inner() } > suspend fun inner() = mutex.withLock { "никогда" } // тот же мьютекс > } > > fun main() = runBlocking { > val result = withTimeoutOrNull(100) { Service().outer() } > println("повисло: ${result == null}") // повисло: true > } > ``` > Публичный метод, берущий лок, не должен вызывать другой публичный метод того же класса. Выносите логику в приватные методы без блокировки. > > **`synchronized` в корутине блокирует поток диспетчера.** Результат будет верным, но пропускная способность упадёт, а на `limitedParallelism(1)` возможен deadlock. Используйте `Mutex`. > > **`suspend` внутри `synchronized` не компилируется** - и это хорошо. Попытка обойти запрет через `runBlocking` внутри `synchronized` создаёт именно тот deadlock, от которого защищает компилятор. > > **Отмена внутри `withLock` освобождает лок, но работа может остаться незавершённой:** > ```kotlin > import kotlinx.coroutines.* > import kotlinx.coroutines.sync.Mutex > import kotlinx.coroutines.sync.withLock > > fun main() = runBlocking { > val mutex = Mutex() > var step = 0 > val job = launch { > mutex.withLock { > step = 1 > delay(1000) // здесь отмена > step = 2 // не выполнится > } > } > delay(20) > job.cancelAndJoin() > println("лок свободен: ${!mutex.isLocked}, состояние на середине: ${step == 1}") > // лок свободен: true, состояние на середине: true > } > ``` > Критическая секция прервалась на середине, оставив несогласованное состояние. Либо делайте секцию атомарной (одна операция `copy`), либо восстанавливайте инвариант в `finally`. > > **`AtomicInteger` защищает одну переменную, а не инвариант:** > ```kotlin > import kotlinx.coroutines.* > import java.util.concurrent.atomic.AtomicInteger > > fun main() = runBlocking { > val total = AtomicInteger(0) > val errors = AtomicInteger(0) > withContext(Dispatchers.Default) { > List(100) { launch { total.incrementAndGet(); errors.incrementAndGet() } } > .forEach { it.join() } > } > println("каждый атомарен: ${total.get()} ${errors.get()}") > // каждый атомарен: 100 100 > } > ``` > Итог верный, но в любой момент между двумя инкрементами снимок несогласован: читатель может увидеть `total = 50, errors = 49`. Для связанных значений нужен один объект состояния. > > **`ConcurrentHashMap` не делает составные операции атомарными.** `if (!map.containsKey(k)) map[k] = v` - гонка. Нужны `computeIfAbsent`, `putIfAbsent` или `merge`. > > **Обычный `mutableListOf` в корутинах небезопасен**, даже если кажется, что обращения редки. `ArrayList.add` при конкурентном вызове может потерять элемент или бросить `ArrayIndexOutOfBoundsException` из-за состояния расширения массива. > > **Мелкозернистый конфайнмент убивает производительность.** `withContext(single) { counter++ }` внутри цикла - это переключение контекста на каждую операцию. Выносите весь блок работы в один `withContext`. > > **Два мьютекса в разном порядке - deadlock**, как и в Java. Правило то же: фиксированный порядок захвата или единый лок. > > **`Semaphore` с утечкой разрешений исчерпывается.** Ручная пара `acquire()`/`release()` без `try/finally` при исключении потеряет разрешение навсегда. Используйте `withPermit`. > > **`limitedParallelism(1)` - не замена `Mutex`.** Он гарантирует один поток для **своих** задач, но состояние может меняться и из других контекстов. Конфайнмент работает только если **все** обращения идут через этот диспетчер. > > **`@Volatile` не делает `++` атомарным.** Он даёт только видимость записи. `@Volatile var counter = 0` с `counter++` из нескольких корутин теряет инкременты точно так же, как обычное поле. > > **Актор сериализует все операции.** Если команд много, одна корутина станет узким местом - её очередь будет расти. Для высокой нагрузки нужны шардирование по ключу или атомарные типы. Далее: [Корутины против потоков](Корутины%20против%20потоков.md)