@@ -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-специфика
0 commit comments