517 lines
29 KiB
Markdown
517 lines
29 KiB
Markdown
---
|
||
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<String>())
|
||
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<Int, String>()
|
||
|
||
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<Boolean>) : AccountCommand
|
||
data class Balance(val reply: CompletableDeferred<Int>) : AccountCommand
|
||
}
|
||
|
||
fun main() = runBlocking {
|
||
val commands = Channel<AccountCommand>()
|
||
|
||
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<Boolean>()
|
||
commands.send(AccountCommand.Withdraw(500, ok))
|
||
println("списание прошло: ${ok.await()}")
|
||
|
||
val tooMuch = CompletableDeferred<Boolean>()
|
||
commands.send(AccountCommand.Withdraw(10_000, tooMuch))
|
||
println("превышение отклонено: ${!tooMuch.await()}")
|
||
|
||
val balance = CompletableDeferred<Int>()
|
||
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["Состояние меняют<br/>несколько корутин"] --> B{"Можно вообще<br/>не разделять?"}
|
||
B -->|"да"| C["локальная переменная<br/>или актор"]
|
||
B -->|"нет"| D{"За состоянием<br/>наблюдают?"}
|
||
D -->|"да"| E["MutableStateFlow<br/>+ update"]
|
||
D -->|"нет"| F{"Одна переменная?"}
|
||
F -->|"да"| G["AtomicInteger / AtomicReference"]
|
||
F -->|"нет, составная операция"| H{"Нужен suspend<br/>внутри секции?"}
|
||
H -->|"да"| I["Mutex.withLock"]
|
||
H -->|"нет"| J["Mutex или<br/>конфайнмент в поток"]
|
||
```
|
||
|
||
## Неизменяемость убирает проблему
|
||
```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<Int>()
|
||
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<Int>()
|
||
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<Int, Int>()
|
||
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)
|