Mutex и Semaphore для общего состояния
Защищайте общее изменяемое состояние с помощью Mutex и ограничивайте параллелизм с помощью Semaphore.
«Mutex и Semaphore для общего состояния» — бесплатный урок Kotlin Academy на CoddyKit. Это урок 3 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Kotlin Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Kotlin Academy содержит 4 уроков всего.
Проблема общего состояния
Сопрограммы в нескольких потоках могут повредить общее изменяемое состояние. Без синхронизации одновременное увеличение значения приводит к неверным результатам.
import kotlinx.coroutines.*
fun main() = runBlocking {
var counter = 0
val jobs = List(1000) {
launch(Dispatchers.Default) { counter++ } // race condition!
}
jobs.forEach { it.join() }
println("Expected 1000, got: $counter") // likely wrong
}Основы взаимной блокировки
Mutex — это блокировка взаимного исключения для сопрограмм. Она приостанавливает, а не блокирует вызывающий код, пока блокировка не станет доступной.
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.*
fun main() = runBlocking {
val mutex = Mutex()
var counter = 0
val jobs = List(1000) {
launch(Dispatchers.Default) {
mutex.withLock { counter++ } // safe
}
}
jobs.forEach { it.join() }
println("Counter: $counter") // 1000
}Расширение withLock
mutex.withLock { ... } получает блокировку, выполняет блок и освобождает её, даже если произошло исключение или отмена.
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.*
fun main() = runBlocking {
val mutex = Mutex()
var list = mutableListOf<Int>()
List(10) {
launch {
mutex.withLock {
list.add(it)
}
}
}.forEach { it.join() }
println(list.sorted())
}lock / unlock вручную
Для более точного управления можно напрямую вызывать lock() и unlock(), но предпочитайте withLock, чтобы не забыть освободить блокировку.
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.*
fun main() = runBlocking {
val mutex = Mutex()
mutex.lock()
try {
println("Critical section")
} finally {
mutex.unlock() // must always release
}
}Взаимная блокировка не является реентерабельной
Mutex в Kotlin не является реентерабельной. Повторный вызов lock() из той же сопрограммы приведёт к взаимной блокировке. Используйте счётчик или измените структуру кода, чтобы избежать вложенных блокировок.
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.*
fun main() = runBlocking {
val mutex = Mutex()
// This deadlocks:
// mutex.withLock { mutex.withLock { println("never") } }
mutex.withLock {
println("Acquired once — safe")
}
}Основы семафоров
Semaphore(n) позволяет не более чем n сопрограммам одновременно продолжить выполнение. Это похоже на систему билетов с n доступными местами.
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.*
fun main() = runBlocking {
val semaphore = Semaphore(3) // max 3 concurrent
List(10) { i ->
launch {
semaphore.withPermit {
println("Concurrent task $i"); delay(100)
}
}
}.forEach { it.join() }
}Расширение withPermit
semaphore.withPermit { ... } получает разрешение, выполняет блок и освобождает разрешение. Это безопасный для сопрограмм эквивалент попытки получить разрешение семафора.
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.*
fun main() = runBlocking {
val sem = Semaphore(2)
val results = mutableListOf<String>()
List(5) { i ->
launch {
sem.withPermit {
delay(50)
synchronized(results) { results.add("task-$i") }
}
}
}.forEach { it.join() }
println(results)
}Ограничение частоты с помощью семафора
Используйте семафор, чтобы ограничивать количество одновременных вызовов API: разрешайте не более N запросов за раз.
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.*
val apiSemaphore = Semaphore(5) // max 5 concurrent requests
suspend fun callApi(id: Int): String {
return apiSemaphore.withPermit {
delay(100) // simulate network
"Response-$id"
}
}
fun main() = runBlocking {
val results = (1..20).map { async { callApi(it) } }.awaitAll()
println("Got ${results.size} responses")
}Взаимная блокировка и семафор
Mutex: двоичный семафор с одним разрешением для исключительного доступа. Semaphore(n): семафор с n разрешениями для ограниченной параллельности. Используйте взаимную блокировку для защиты ресурса, а семафор — для ограничения параллельности.
import kotlinx.coroutines.sync.*
// Mutex = Semaphore(1) for exclusive access
// Semaphore(n) = n-way concurrency limit
val exclusive = Mutex() // one at a time
val limited = Semaphore(3) // three at a time
fun main() { println("Mutex for exclusion; Semaphore for rate limiting") }Атомарный счётчик с взаимной блокировкой
Реализуйте потокобезопасный класс счётчика, используя взаимную блокировку для защиты операций увеличения и чтения.
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.*
class AtomicCounter {
private val mutex = Mutex()
private var count = 0
suspend fun increment() = mutex.withLock { count++ }
suspend fun get() = mutex.withLock { count }
}
fun main() = runBlocking {
val counter = AtomicCounter()
List(100) { launch { counter.increment() } }.forEach { it.join() }
println("Count: ${counter.get()}")
}Альтернатива: однопоточный диспетчер
Вместо взаимной блокировки изолируйте изменяемое состояние в однопоточном диспетчере. На этом диспетчере одновременно выполняется только одна сопрограмма, поэтому доступ к состоянию по своей природе безопасен.
import kotlinx.coroutines.*
fun main() = runBlocking {
val singleThread = newSingleThreadContext("CounterThread")
var counter = 0
List(1000) {
launch(singleThread) { counter++ }
}.forEach { it.join() }
println("Counter: $counter") // 1000, no mutex needed
singleThread.close()
}Быстрая проверка
В чём состоит основное различие между Mutex и Semaphore?
Итоги
Взаимная блокировка обеспечивает исключительный доступ к общему состоянию. Предпочитайте withLock. Семафор с числом разрешений n ограничивает параллельность значением n. Оба механизма приостанавливают, а не блокируют ожидающие сопрограммы. В простых случаях однопоточный диспетчер часто оказывается более понятным решением.
Часто задаваемые вопросы
Урок «Mutex и Semaphore для общего состояния» бесплатный?
Да — полный текст урока «Mutex и Semaphore для общего состояния» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Kotlin Academy, подпишись на CoddyKit PRO. Курс Kotlin Academy содержит 4 уроков всего.
Чему я научусь в уроке «Mutex и Semaphore для общего состояния»?
Защищайте общее изменяемое состояние с помощью Mutex и ограничивайте параллелизм с помощью Semaphore. Ты практикуешь Kotlin Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать Kotlin Academy?
Предыдущий опыт не требуется. Kotlin Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 3 из 4.
Сколько времени занимает урок «Mutex и Semaphore для общего состояния»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке Kotlin Academy?
Да. Каждый урок Kotlin Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Основы каналов: send, receive и close
- Типы каналов: Rendezvous, Buffered, Conflated, Unlimited
- Mutex и Semaphore для общего состояния
- Акторы и структурированное управление состоянием