Skip to content

Commit 48d8700

Browse files
committed
updated async and coroutines chapters with mutex examples
1 parent d822281 commit 48d8700

3 files changed

Lines changed: 240 additions & 1 deletion

File tree

08-coroutines-android.md

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,116 @@ suspend fun <T, R> Iterable<T>.mapParallel(
373373

374374
## 4.3. Дедупликация одновременных запросов (single-flight)
375375

376+
### Что делает `Mutex`
377+
378+
`kotlinx.coroutines.sync.Mutex` защищает критическую секцию так, чтобы одновременно внутри неё
379+
находилась только одна корутина. Если mutex уже занят, следующая корутина **приостанавливается**,
380+
а не блокирует текущий поток. Поэтому `Mutex` подходит для coroutine-кода, в отличие от попытки
381+
удерживать `synchronized`/`ReentrantLock` через suspension point.
382+
383+
Обычный шаблон:
384+
385+
```kotlin
386+
private val mutex = Mutex()
387+
388+
suspend fun updateSafely() {
389+
mutex.withLock {
390+
// Чтение и изменение общего mutable state.
391+
}
392+
}
393+
```
394+
395+
`withLock` освобождает mutex в `finally`, в том числе если код внутри бросил исключение или корутина
396+
была отменена. Ожидание свободного mutex cancellable: отменённая ожидающая корутина перестаёт
397+
претендовать на вход.
398+
399+
Важные свойства:
400+
401+
- `Mutex` защищает **инвариант**, а не отдельную строку кода;
402+
- он не привязан к потоку: корутина может приостановиться и продолжиться на другом;
403+
- он не реентерабельный — повторный `withLock` того же mutex из критической секции приводит к зависанию;
404+
- он сериализует работу, поэтому долгий network/disk вызов под общим mutex допустим только осознанно;
405+
- для нескольких одновременных разрешений нужен `Semaphore`, а для простой числовой операции часто
406+
лучше atomic или `MutableStateFlow.update`.
407+
408+
### Пример 1. Счётчик с составной операцией
409+
410+
`counter++` — это чтение, вычисление и запись, а не одна атомарная операция:
411+
412+
```kotlin
413+
class SafeCounter {
414+
private val mutex = Mutex()
415+
private var value = 0
416+
417+
suspend fun increment() {
418+
mutex.withLock {
419+
value += 1
420+
}
421+
}
422+
423+
suspend fun current(): Int =
424+
mutex.withLock { value }
425+
}
426+
427+
suspend fun countConcurrently(): Int = coroutineScope {
428+
val counter = SafeCounter()
429+
430+
repeat(1_000) {
431+
launch(Dispatchers.Default) {
432+
counter.increment()
433+
}
434+
}
435+
436+
// coroutineScope дождётся всех launch перед возвратом.
437+
counter
438+
}.current()
439+
```
440+
441+
Для одного счётчика `AtomicInteger` проще и быстрее. Пример показывает базовую механику; `Mutex`
442+
становится особенно полезен, когда одним действием нужно согласованно изменить несколько значений.
443+
444+
### Пример 2. Атомарный перевод между счетами
445+
446+
Проверка баланса и обе записи составляют один инвариант, поэтому находятся в одной критической секции:
447+
448+
```kotlin
449+
class Wallet {
450+
private val mutex = Mutex()
451+
private val balances = mutableMapOf<String, Long>()
452+
453+
suspend fun deposit(account: String, amount: Long) {
454+
require(amount > 0)
455+
mutex.withLock {
456+
balances[account] = balances.getOrDefault(account, 0L) + amount
457+
}
458+
}
459+
460+
suspend fun transfer(
461+
from: String,
462+
to: String,
463+
amount: Long,
464+
) {
465+
require(amount > 0)
466+
467+
mutex.withLock {
468+
val sourceBalance = balances.getOrDefault(from, 0L)
469+
require(sourceBalance >= amount) { "Insufficient funds" }
470+
471+
balances[from] = sourceBalance - amount
472+
balances[to] = balances.getOrDefault(to, 0L) + amount
473+
}
474+
}
475+
476+
suspend fun snapshot(): Map<String, Long> =
477+
mutex.withLock { balances.toMap() }
478+
}
479+
```
480+
481+
Если защищать списание и зачисление разными lock-вызовами, другая корутина сможет увидеть
482+
промежуточное состояние, в котором деньги уже списаны, но ещё не зачислены.
483+
484+
### Продвинутый пример: single-flight
485+
376486
```kotlin
377487
class SingleFlight<K, V>(private val scope: CoroutineScope) {
378488
private val mutex = Mutex()

11-concurrency-deep.md

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,135 @@ Kotlin, кстати, защищает от родственной ошибки
523523
И обязательная оговорка: корутины гонки **не устраняют**. Внутри одной корутины код последователен,
524524
но несколько дочерних корутин работают параллельно и на разных потоках.
525525

526+
## 12.1. `Mutex`: семантика и правильное применение
527+
528+
`kotlinx.coroutines.sync.Mutex` — coroutine-friendly взаимное исключение. Свободный mutex захватывается
529+
сразу, а при занятом mutex вызывающая корутина приостанавливается, освобождая поток для другой работы.
530+
Это главное отличие от `synchronized` и `ReentrantLock.lock()`, которые блокируют поток ожидания.
531+
532+
Предпочтительный API — `withLock`:
533+
534+
```kotlin
535+
private val mutex = Mutex()
536+
537+
suspend fun changeState() {
538+
mutex.withLock {
539+
// Критическая секция.
540+
}
541+
}
542+
```
543+
544+
Ручная пара `lock()`/`unlock()` нужна редко: легко забыть `unlock()` на ветке ошибки. `withLock`
545+
гарантирует освобождение через `finally`. Ожидание `lock` cancellable, но после успешного входа
546+
защищённый код подчиняется обычным правилам cancellation.
547+
548+
На JVM успешный `unlock` happens-before последующего успешного `lock` того же mutex. Это обеспечивает
549+
видимость записей между критическими секциями. Неуспешный `tryLock` такого memory effect не даёт.
550+
551+
В отличие от `synchronized`, `Mutex` не реентерабельный. Правильная композиция — один раз взять mutex,
552+
а внутреннюю работу вынести в функцию, которая предполагает уже захваченный lock:
553+
554+
```kotlin
555+
class Inventory {
556+
private val mutex = Mutex()
557+
private val quantities = mutableMapOf<String, Int>()
558+
559+
suspend fun add(
560+
sku: String,
561+
amount: Int,
562+
) {
563+
require(amount > 0)
564+
mutex.withLock {
565+
addLocked(sku, amount)
566+
}
567+
}
568+
569+
suspend fun addAndGetTotal(
570+
sku: String,
571+
amount: Int,
572+
): Int {
573+
require(amount > 0)
574+
return mutex.withLock {
575+
addLocked(sku, amount) // не пытается взять mutex повторно
576+
quantities.values.sum()
577+
}
578+
}
579+
580+
// Вызывать только из mutex.withLock.
581+
private fun addLocked(
582+
sku: String,
583+
amount: Int,
584+
) {
585+
quantities[sku] =
586+
quantities.getOrDefault(sku, 0) + amount
587+
}
588+
}
589+
```
590+
591+
### Пример 1. Ровно одно обновление токена
592+
593+
Иногда suspend-вызов под mutex — осознанная часть инварианта. Пока один запрос обновляет токен,
594+
остальные должны дождаться того же результата, а не запустить параллельный refresh:
595+
596+
```kotlin
597+
class TokenRepository(
598+
private val api: AuthApi,
599+
private val clock: Clock,
600+
) {
601+
private val mutex = Mutex()
602+
private var token: Token? = null
603+
604+
suspend fun validToken(): Token =
605+
mutex.withLock {
606+
token
607+
?.takeIf { it.expiresAt > clock.now() }
608+
?: api.refreshToken().also { refreshed ->
609+
token = refreshed
610+
}
611+
}
612+
}
613+
```
614+
615+
Здесь network-вызов намеренно удерживает mutex: это гарантирует один refresh. Цена — все чтения токена
616+
сериализованы. Для независимых ключей или сложного lifecycle лучше keyed mutex/single-flight, чтобы
617+
одна медленная операция не останавливала несвязанные запросы.
618+
619+
### Пример 2. Чужой код вызывается после критической секции
620+
621+
Callback, listener или пользовательскую suspend-функцию не следует вызывать под mutex без строгой
622+
необходимости: неизвестный код может долго выполняться или попытаться войти в тот же объект.
623+
Под lock создают согласованный snapshot, а уведомляют снаружи:
624+
625+
```kotlin
626+
class SettingsStore {
627+
private val mutex = Mutex()
628+
private val values = mutableMapOf<String, String>()
629+
630+
suspend fun update(
631+
key: String,
632+
value: String,
633+
onChanged: suspend (Map<String, String>) -> Unit,
634+
) {
635+
val snapshot = mutex.withLock {
636+
values[key] = value
637+
values.toMap()
638+
}
639+
640+
// Mutex уже освобождён: callback не удерживает критическую секцию.
641+
onChanged(snapshot)
642+
}
643+
}
644+
```
645+
646+
Такой код защищает mutable map, минимизирует время удержания mutex и не допускает утечки изменяемой
647+
коллекции наружу. При этом порядок завершения callbacks уже не сериализован — если он важен как часть
648+
контракта, нужна отдельная модель доставки событий (`Channel`, actor/confinement или явно
649+
спроектированная очередь).
650+
651+
`Mutex` выбирают для составного mutable-инварианта в coroutine-коде. Для одной переменной часто лучше
652+
atomic/CAS, для ограничения параллелизма — `Semaphore`, для последовательного владельца состояния —
653+
confinement/actor, а для короткого несуспендящего Java-кода — `synchronized`.
654+
526655
---
527656

528657
# 13. Android-специфика

web/src/generated/materials.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)