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.