Kotlin CoroutinesMiddleTechnical

Что такое Mutex в coroutines и когда его использовать для защиты разделяемого состояния?

Mutex из kotlinx.coroutines.sync — корутин-осознанный замок: lock() приостанавливает корутину без блокировки потока. Используйте withLock { } для защиты разделяемого изменяемого состояния. Mutex не реентерабелен — повторный захват из той же корутины приведёт к дедлоку.

Mutex в Kotlin Coroutines

Mutex (mutual exclusion lock) из пакета kotlinx.coroutines.sync — это корутин-осознанный взаимоисключающий замок. В отличие от java.util.concurrent.locks.ReentrantLock, Mutex.lock() — это suspend-функция: ожидающая корутина приостанавливается, не блокируя поток.

Проблема: состояние гонки

import kotlinx.coroutines.*

var counter = 0

fun main() = runBlocking {
    val jobs = List(1000) {
        launch(Dispatchers.Default) {
            counter++  // НЕ атомарно — data race!
        }
    }
    jobs.forEach { it.join() }
    println(counter)  // результат непредсказуем, не 1000
}

Решение с Mutex

import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock

val mutex = Mutex()
var counter = 0

fun main() = runBlocking {
    val jobs = List(1000) {
        launch(Dispatchers.Default) {
            mutex.withLock {  // suspend, не блокирует поток
                counter++
            }
        }
    }
    jobs.forEach { it.join() }
    println(counter)  // всегда 1000
}

withLock — extension-функция, которая вызывает lock(), выполняет блок и вызывает unlock() в finally, гарантируя освобождение замка даже при исключении.

Низкоуровневый API

val mutex = Mutex()

// Явный lock/unlock
mutex.lock(owner = this)   // suspend
try {
    // критическая секция
} finally {
    mutex.unlock(owner = this)
}

// Неблокирующая попытка захватить замок
if (mutex.tryLock()) {
    try { /* ... */ } finally { mutex.unlock() }
} else {
    // замок занят, делаем что-то другое
}

Когда использовать Mutex

  • Защита изменяемого состояния, доступного из нескольких корутин параллельно.
  • Кэш с ленивой инициализацией, где несколько корутин могут одновременно промахнуться по кэшу.
  • Ограничение параллельного доступа к ресурсу (например, файлу или стороннему SDK).

Альтернативы

  • AtomicInteger / AtomicReference — для примитивных атомарных операций без suspend.
  • Single-threaded dispatcher: Dispatchers.IO.limitedParallelism(1) — состояние меняется только в одном потоке.
  • Actor (obsolete): actor { } устарел в пользу Channel + корутина-обработчик.
  • Channel / StateFlow: для реактивного обновления состояния вместо прямого мутирования.
// Альтернатива: конфайн состояние в один поток
val singleThread = Dispatchers.IO.limitedParallelism(1)
var counter = 0

launch(singleThread) {
    repeat(1000) { counter++ }  // безопасно — один поток
}

Подводные камни

  • Mutex не реентерабелен: если корутина, владеющая замком, попытается захватить его снова — дедлок. В отличие от ReentrantLock, Mutex не поддерживает повторный вход.
  • Не вызывайте suspend внутри lock без withLock: если suspend-функция бросит исключение, а unlock не завершится — замок никогда не освободится. Всегда используйте withLock или try/finally.
  • Мьютекс и отмена: если корутина отменяется, находясь в очереди на lock(), она получает CancellationException и не захватывает замок. Если она уже внутри критической секции — unlock вызывается через finally.
  • Производительность: при высокой конкуренции Mutex создаёт серьёзное bottleneck. Рассмотрите lock-free структуры данных или разбивку состояния.
  • Deadlock при вложенных Mutex: если корутина A держит mutex1 и ждёт mutex2, а корутина B держит mutex2 и ждёт mutex1 — дедлок.
  • withLock не защищает само поле: объявив поле volatile или lateinit, вы не получаете защиты от гонки — нужен именно Mutex вокруг чтения И записи.

Common mistakes

  • Объяснять «Mutex в coroutines» только как синтаксис и не описывать поведение runtime/compiler.
  • Игнорировать важный риск: Держать Mutex вокруг долгого I/O или вызывать внешний callback внутри lock опасно для latency и deadlocks.
  • Давать пример без edge case: отмены, null, recomposition, platform boundary или ошибки.

What the interviewer is testing

  • Формулирует суть темы «Mutex в coroutines» своими словами и связывает ее с кодом.
  • Называет механизм: withLock suspend-ится, пока lock недоступен, и освобождает lock при выходе из блока.
  • Видит production-последствие: Держать Mutex вокруг долгого I/O или вызывать внешний callback внутри lock опасно для latency и deadlocks.

Sources

Related topics