Mutex тормозит корутины. Как перестать лочить и начать житьВы пишете многопоточный код. У вас есть общий ресурс (например, in-memory кэш сессий). Вы знаете, что
synchronized в корутинах - это зло (блокирует системный поток).Вы берете
Mutex и оборачиваете код в mutex.withLock { ... }.Поздравляю, вы сделали лучше. Но под большим contention (высокой конкуренцией) ваш код всё равно будет страдать.
В чем проблема Mutex?
Да, он не блокирует поток. Но
withLock - это suspend функция.Когда 100 корутин ломятся в один лок, 1 получает доступ, а 99 - саспендятся. Саспенд - это не бесплатно. Это сохранение стейт-машины, аллокации
Continuation, а потом дорогой диспетчеризированный возврат (resume) каждой корутины к жизни.🛠 Как от этого уходят Сеньоры? (2 пути)
Путь 1: Lock-free (Для простых стейтов)
Если у вас счетчик или простая ссылка на объект, забудьте про локи. Используйте атомики (CAS-операции — Compare-And-Swap). Они выполняются на уровне железа процессора.
// ❌ Медленно:
val mutex = Mutex()
var count = 0
// ... mutex.withLock { count++ }
// ✅ Летает:
val count = AtomicInteger(0)
// ... count.incrementAndGet()
// 🧠 Для Kotlin-объектов:
val state = MutableStateFlow(InitialState)
// Под капотом использует CAS-цикл без локов!
state.update { it.copy(...) }
Путь 2: Thread Confinement (Для сложной логики)
Если логика внутри лока большая (нужно сходить в мапу, проверить список, обновить 3 переменные) - CAS не спасет.
Вместо того чтобы обносить данные забором из локов, спрячьте данные в один поток.
Вспоминаем наш прошлый пост про
limitedParallelism(1) - это официально рекомендуемая JetBrains замена устаревшим Actors.
class SessionManager {
// Создаем свой "однопоточный" диспетчер
private val stateContext = Dispatchers.Default.limitedParallelism(1)
// Переменная вообще без потокобезопасных оберток!
private val cache = mutableMapOf<String, Session>()
suspend fun updateSession(id: String, data: Session) {
// Все операции с кэшем встают в аккуратную очередь
// на выполнение в один поток. Никаких race conditions.
withContext(stateContext) {
val existing = cache[id]
cache[id] = merge(existing, data)
}
}
}
Почему Confinement (ограничение потока) лучше?
1. Вы избавляетесь от Deadlocks по определению.
2. Процессору гораздо проще прогнать очередь инструкций на одном ядре (работает L1/L2 кэш процессора), чем прыгать между потоками, пытающимися захватить
Mutex.3. Код становится линейным и предсказуемым.
Вывод:
Mutex хорош, когда коллизии редки.Если ресурс "горячий" (в него долбятся постоянно) - изолируйте стейт в
StateFlow (если можно) или в отдельный limitedParallelism(1) контекст (если логика сложная).Признавайтесь, у кого проект усыпан
Mutex() на каждом чихе? 👇📲 Мы в MAX
✍️ @kotlin_lib