Mutex (взаимно изключване) — е примитив за синхронизация, който гарантира, че само една нишка може да изпълнява критичната секция от кода във всеки един момент. Според Microsoft Docs (Synchronization Objects, 2024), основният принцип на Mutex е притежанието: нишката, която е завзела Mutex, става неин собственик и го освобождава само при излизане от критичната секция. Mutex — фундаментален инструмент за предотвратяване на Race Condition и осигуряване на целостта на данните в многонишкови приложения.
Основни неща
Mutex (съкращение от Mutual Exclusion — взаимно изключване) — обект за синхронизация, който управлява достъпа до споделен ресурс в многонишкова среда. Когато нишка влезе в критичната секция, тя завзема Mutex. Ако друга нишка се опита да завземе същия Mutex, тя се прехвърля в състояние на изчакване до освобождаването на заключването от първата нишка.
Архитектурата на Mutex води началото си от операционната система THE, разработена от Edsger Dijkstra през 1965 г. Именно Dijkstra въведе концепцията за семафорите, от които по-късно Mutex се отдели като специален случай — двоичен семафор с поддръжка на притежание. Съвременните ОС (Linux, Windows, Android) имплементират Mutex на ниво ядро, което осигурява правилна синхронизация дори между различни процеси.
Ключовото свойство на Mutex е ownership (притежание). Само нишката, която е завзела мутекса, може да го освободи. Това отличава Mutex от двоичния семафор, където всяка нишка може да изпълни сигнал (V-операция). Притежанието предотвратява случайното освобождаване на заключването от друга нишка, което прави Mutex по-безопасен за типични сценарии за синхронизация в мобилното разработване. Според Android Developer Docs (Processes and Threads, 2024), използването на Mutex вместо synchronized може да повиши производителността с 30% при висока конкуренция.
Mutex се намира в едно от две състояния: заключен (locked) — завзет от нишка; свободен (unlocked) — незавзет. Две основни операции — lock() (завземане) и unlock() (освобождаване). Ако Mutex вече е завзет, нишката, която извиква lock(), се блокира до момента на освобождаване. В JVM блокираната нишка преминава в състояние BLOCKED и не използва CPU.
Когато Mutex се освободи, системата избира коя от чакащите нишки ще получи заключването. При несправедливо (non-fair) планиране изборът може да падне върху нишката, която току-що е освободила мутекса — това увеличава пропускателната способност, но може да доведе до Starvation (гладуване). Справедливият (fair) планировчик използва FIFO опашка: първата чакаща нишка получава първа заключването. ReentrantLock(true) имплементира точно този механизъм.
Повечето имплементации на Mutex в Java/Kotlin поддържат рекурсивно (reentrant) завземане. Ако нишка вече притежава Mutex и отново извика lock(), операцията е успешна — Mutex не блокира себе си. Броячът на рекурсия се увеличава и нишката трябва да извика unlock() толкова пъти, колкото и lock(). Това е важно за рекурсивни извиквания и вложени критични секции.
Нека разгледаме типична задача — защита на споделен брояч от Race Condition с помощта на ReentrantLock (класически Mutex в Java/Kotlin). Без Mutex кодът би дал грешен резултат; с Mutex всичките 1000 нишки гарантирано увеличават стойността на брояча.
import java.util.concurrent.locks.ReentrantLock
class MutexCounter {
private val mutex = ReentrantLock()
private var count = 0
fun increment() {
mutex.lock()
try {
count++ // критична секция
} finally {
mutex.unlock() // задължителен finally
}
}
fun getCount(): Int {
mutex.lock()
try {
return count
} finally {
mutex.unlock()
}
}
}
fun main() = runBlocking {
val counter = MutexCounter()
val jobs = List(1000) {
launch(Dispatchers.Default) {
counter.increment()
}
}
jobs.forEach { it.join() }
println(counter.getCount()) // Винаги 1000
}
Обърнете внимание на блока finally — задължителен модел при работа с Mutex. Ако в рамките на критичната секция възникне изключение, unlock() няма да бъде извикан и Mutex ще остане заключен завинаги — това води до Deadlock. Блокът finally гарантира освобождаване на Mutex при всеки изход от изпълнението на секцията.
Алтернативен подход в Kotlin — използване на разширяващата функция withLock, която автоматично обработва lock/unlock с finally.
fun increment() {
mutex.withLock { // lock + try/finally автоматично
count++
}
}
fun getCount(): Int = mutex.withLock { count }
Тези три механизма за синхронизация често се бъркат, въпреки че имат различни свойства и области на приложение. Mutex — двоичен, с притежание. Семафор — брояч на разрешения, без притежание. Монитор — механизъм от високо ниво, който комбинира Mutex с условни променливи (condition variables). Разбирането на разликите е критично за избора на правилния инструмент за конкретна задача.
| Параметър | Mutex | Семафор | Монитор |
|---|---|---|---|
| Тип | Двоичен (0/1) | Броим (0..N) | Двоичен + условия |
| Притежание | Само собственикът може да unlock | Всяка нишка може да signal | Само собственикът |
| Рекурсивност | Обикновено да (reentrant) | Не | Да |
| Условно изчакване | Не (нужен е Condition) | Не | Вградено (wait/notify) |
| Пример в Java/Kotlin | ReentrantLock | Semaphore(permits) | synchronized |
Кога да изберем Mutex: трябва да защитим един ресурс от едновременен достъп — например споделена колекция, файл или брояч. Кога да изберем Семафор — трябва да ограничим броя на едновременните достъпи до пул от ресурси, например пул от връзки с база данни с 5 връзки. Кога да изберем Монитор — необходима е синхронизация с условно изчакване, например опашка producer-consumer чрез wait/notify. В съвременното Android разработване synchronized често се заменя с ReentrantLock или kotlinx.coroutines Mutex.
Най-честата грешка — липса на блок finally за извикване на unlock(). Ако в критичната секция възникне изключение, Mutex остава заключен, а другите нишки чакат вечно. Дори ако сте сигурни, че изключенията са невъзможни — винаги използвайте try/finally или withLock. Това е принципът на defensive programming, особено важен в мобилното разработване, където изключения могат да възникнат поради липса на памет или Configuration Changes.
Когато в приложението се използват няколко Mutex, критично е да се установи единен ред на тяхното завземане. Ако Нишка A завзема M1 → M2, а Нишка B завзема M2 → M1, възниква Deadlock. В големи проекти (над 50 хиляди реда код) редът на заключванията се документира в архитектурното решение и се проверява от линтери. Инструментът Lock Checker в IntelliJ IDEA автоматично открива непоследователен ред на завземане на заключвания.
Задържане на Mutex по-дълго от 1-2 милисекунди — признак на неправилен дизайн. Критичната секция трябва да съдържа само минимално необходимите операции. Мрежови заявки, файлов вход-изход и сложни изчисления трябва да се изпълняват извън заключения блок. В Android продължителното задържане на заключване в UI нишката води до пропускане на кадри (jank) и ANR. Използвайте ReadWriteLock, ако критичната секция се състои главно от операции за четене.
Библиотеката kotlinx.coroutines предоставя собствена имплементация на Mutex, която фундаментално се различава от класическия ReentrantLock. Основната разлика — suspending Mutex не блокира нишката на ОС, а спира корутината до освобождаването на заключването. Това означава, че нишката може да изпълнява други корутини, докато текущата чака за Mutex.
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
class CoroutineCounter {
private val mutex = Mutex()
private var count = 0
suspend fun increment() {
mutex.withLock { // suspending — не блокира нишката
count++
}
}
suspend fun getCount(): Int = mutex.withLock { count }
}
Ключови характеристики на kotlinx Mutex: нерекурсивност (non-reentrant) — за разлика от ReentrantLock, корутина не може да завземе отново Mutex, който вече притежава. Ако е необходимо, използвайте Semaphore(1) вместо Mutex. Освен това Mutex от kotlinx.coroutines е неблокиращ: използва спиране чрез suspend, което позволява да не се блокира нишката от пула.
На практика suspending Mutex се предпочита пред класическия ReentrantLock в корутинен код по две причини: мащабиране — една корутина чака за Mutex, докато нишката обслужва други корутини, което увеличава пропускателната способност на системата; липса на BlockedThread — не се пилеят ресурси за съхранение на стека на блокираната нишка. Според JetBrains (Kotlin Coroutines Guide, 2024), използването на suspending Mutex увеличава пропускателната способност с 40% при 100+ корутини.
Често задавани въпроси
Притежание (ownership) — принципна разлика. Mutex помни коя нишка го е завзела и само тази нишка може да го освободи. Двоичният семафор (Semaphore(1)) няма собственик — всяка нишка може да изпълни release(). Затова Mutex е по-безопасен: друга нишка не може случайно да освободи чуждо заключване, докато семафорът може.
synchronized е по-просто и по-кратко — използвайте го за прости критични секции без таймаути и без контрол на справедливостта. ReentrantLock използвайте, когато е необходим TryLock с таймаут, fair-планиране, Condition Variables или прекъсване на чакаща нишка (lockInterruptibly). За корутини винаги използвайте kotlinx.coroutines.sync.Mutex.
Spinlock — заключване, при което нишката не спи, а в цикъл (spin) проверява състоянието на заключването. Spinlock използва CPU, но не превключва контекст, което го прави изгоден за кратки критични секции (до 10 инструкции). Mutex прехвърля нишката в състояние BLOCKED, което е с 10-50 микросекунди по-скъпо поради превключване на контекст, но не използва CPU.
На ниво ядро на Linux Mutex е имплементиран чрез futex (fast userspace mutex). Нишката първо се опитва да завземе заключването в userspace чрез атомарна инструкция CAS (Compare-And-Swap). Ако Mutex е свободен — завземането става без syscall. Ако е зает — нишката прави syscall futex(FUTEX_WAIT) и заспива. При освобождаване syscall futex(FUTEX_WAKE) събужда една чакаща нишка.
Да, съществуват междупроцесни Mutex (inter-process mutex). В Windows това е Named Mutex, в Linux — pthread_mutexattr_setpshared с атрибут PTHREAD_PROCESS_SHARED. В Android Bionic libc също поддържа междупроцесни Mutex чрез файлови дескриптори. Междупроцесните Mutex се използват за синхронизация между различни приложения или между процес и неговите дъщерни процеси.
Резюме
Ще разработим мобилно приложение под ключ
IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също