Mutex (међусобно искључивање) — примитив синхронизације који гарантује да само једна нит може извршавати критичну секцију кода у сваком тренутку. Према Microsoft Docs (Synchronization Objects, 2024), основни принцип Mutex-а је власништво: нит која је преузела Mutex постаје његов власник и ослобађа га тек при изласку из критичне секције. Mutex — фундаментални алат за спречавање Race Condition и обезбеђивање интегритета података у више nitnim aplikacijama.
Главно
Mutex (скраћеница од Mutual Exclusion — међусобно искључивање) — објекат синхронизације који управља приступом заједничком ресурсу у више nitnom okruženju. Када нит уђе у критичну секцију, преузима 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-у — употреба extension функције withLock, која аутоматски обрађује lock/unlock са finally.
fun increment() {
mutex.withLock { // lock + try/finally аутоматски
count++
}
}
fun getCount(): Int = mutex.withLock { count }
Ова три механизма синхронизације се често мешају, иако имају различита својства и области примене. Mutex — бинарни, са власништвом. Semaphore — бројач дозвола, без власништва. Monitor — високоризиони механизам који обједињује Mutex са условним променљивим (condition variables). Разумевање разлика је критично за избор правог алата за одређени задатак.
| Параметар | Mutex | Semaphore | Monitor |
|---|---|---|---|
| Тип | Бинарни (0/1) | Бројиви (0..N) | Бинарни + услови |
| Власништво | Само власник може unlock | Било која нит може signal | Само власник |
| Рекурзивност | Обично да (reentrant) | Не | Да |
| Условно чекање | Не (потребан Condition) | Не | Уграђено (wait/notify) |
| Пример у Java/Kotlin | ReentrantLock | Semaphore(permits) | synchronized |
Када изабрати Mutex: потребно је заштитити један ресурс од истовременог приступа — на пример, заједничку колекцију, датотеку или бројач. Када изабрати Semaphore — потребно је ограничити број истовремених приступа базену ресурса, на пример базен веза са базом података на 5 веза. Када изабрати Monitor — потребна је синхронизација са условним чекањем, на пример producer-consumer ред кроз wait/notify. У савременом Android развоју synchronized се често замењује ReentrantLock-ом или kotlinx.coroutines Mutex-ом.
Најчешћа грешка — одсуство finally блока за позив unlock(). Ако у критичној секцији дође до изузетка, Mutex остаје закључан, а друге нити чекају заувек. Чак и ако сте сигурни да су изузеци немогући — увек користите try/finally или withLock. Ово је принцип defensive programming, посебно важан у мобилном развоју, где изузеци могу настати због недостатка меморије или Configuration Changes.
Када се у апликацији користи више Mutex-а, критично је успоставити јединствени редослед њиховог преузимања. Ако Nit A преузима M1 → M2, а Nit B преузима M2 → M1, настаје Deadlock. У великим пројектима (преко 50 хиљада линија кода) редослед закључавања се документује у архитектонској одлуци и проверава линтерима. Алат Lock Checker у IntelliJ IDEA аутоматски открива недоследан редослед преузимања закључавања.
Држање Mutex-а дуже од 1-2 милисекунде — знак неправилног дизајна. Критична секција треба да садржи само минимално неопходне операције. Мрежни захтеви, улазно-излазне операције са датотекама и сложена израчунавања треба да се извршавају ван блокираног блока. У Android-у дуго држање закључавања у UI нити доводи до испустања кадрова (jank) и ANR-а. Користите ReadWriteLock ако се критична секција углавном састоји од операција читања.
Библиотека kotlinx.coroutines пружа сопствену имплементацију Mutex-а која се фундаментално разликује од класичног ReentrantLock-а. Главна разлика — suspending Mutex не блокира нит оперативног система, већ суспендује corutinu до тренутка ослобађања закључавања. То значи да нит може извршавати друге corutine док тренутна чека на 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-а, corutina не може поново преузети Mutex којим већ влада. Ако је то потребно, користите Semaphore(1) уместо Mutex-а. Поред тога, Mutex из kotlinx.coroutines-а је неблокирајући: користи суспендовање кроз suspend, што омогућава да не блокира нит pool-a.
У пракси, suspending Mutex је пожељнији од класичног ReentrantLock-а у corutine коду из два разлога: скалирање — једна corutina чека на Mutex, а нит опслужује друге corutine, што повећава пропусну моћ система; одсуство BlockedThread — не троши се ресурс на чување стека блокиране нити. Према JetBrains (Kotlin Coroutines Guide, 2024), употреба suspending Mutex-а повећава пропусну моћ за 40% при 100+ corutina.
Често постављана питања
Власништво (ownership) — суштинска разлика. Mutex памти која га је нит преузела и само та нит га може ослободити. Бинарни семафор (Semaphore(1)) нема власника — било која нит може извршити release(). Зато је Mutex сигурнији: друга нит не може случајно ослободити туђе закључавање, а семафор може.
synchronized је једноставнији и краћи — користите га за једноставне критичне секције без временских ограничења и без контроле праведности. ReentrantLock користите када је потребан TryLock са тајмаутом, fair-планирање, Condition Variables или прекидање нити у чекању (lockInterruptibly). За corutine увек користите 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. године. Саветоваћемо вас и предложити најбоље решење.
Прочитајте такође