Mutex (excludere mutuală) — este un primitiv de sincronizare care garantează că un singur fir de execuție poate executa secțiunea critică de cod în fiecare moment. Conform Microsoft Docs (Synchronization Objects, 2024), principiul de bază al Mutex este posesia: firul care a preluat Mutex devine proprietarul său și îl eliberează doar la ieșirea din secțiunea critică. Mutex — un instrument fundamental pentru prevenirea Race Condition și asigurarea integrității datelor în aplicațiile multi-thread.
Principalele
Mutex (prescurtare de la Mutual Exclusion — excludere mutuală) — este un obiect de sincronizare care gestionează accesul la o resursă partajată într-un mediu multi-thread. Când un fir de execuție intră în secțiunea critică, preia Mutex. Dacă un alt fir încearcă să preia același Mutex, acesta este trecut în starea de așteptare până la eliberarea blocajului de către primul fir.
Arhitectura Mutex își are originea în sistemul de operare THE, dezvoltat de Edsger Dijkstra în 1965. Exact Dijkstra a introdus conceptul de semafoare, din care mai târziu s-a desprins Mutex ca un caz particular — semafor binar cu suport pentru posesie. SO-urile moderne (Linux, Windows, Android) implementează Mutex la nivelul nucleului, ceea ce asigură funcționarea corectă a sincronizării chiar și între procese diferite.
Proprietatea cheie a Mutex este ownership (posesia). Doar firul care a preluat mutexul îl poate elibera. Aceasta deosebește Mutex de semaforul binar, unde orice fir poate executa un semnal (operația V). Posesia previne eliberarea accidentală a blocajului de către un alt fir, ceea ce face Mutex mai sigur pentru scenariile tipice de sincronizare în dezvoltarea mobilă. Conform Android Developer Docs (Processes and Threads, 2024), utilizarea Mutex în loc de synchronized poate crește performanța cu 30% la concurență ridicată.
Mutex se află în una din două stări: blocat (locked) — preluat de un fir; liber (unlocked) — nepreluat. Două operații de bază — lock() (preluare) și unlock() (eliberare). Dacă Mutex este deja preluat, firul care apelează lock() se blochează până la eliberare. În JVM, firul blocat trece în starea BLOCKED și nu consumă CPU.
Când Mutex este eliberat, sistemul alege care dintre firele în așteptare va primi blocajul. La planificarea nedreaptă (non-fair) alegerea poate cădea asupra firului care tocmai a eliberat mutexul — aceasta crește debitul, dar poate duce la Starvation (înfoșare). Planificatorul drept (fair) utilizează o coadă FIFO: primul fir în așteptare primește primul blocajul. ReentrantLock(true) implementează exact acest mecanism.
Majoritatea implementărilor de Mutex în Java/Kotlin suportă preluarea recursivă (reentrant). Dacă un fir deține deja Mutex și apelează din nou lock(), operația este reușită — Mutex nu se blochează singur. Contorul de recursivitate crește, iar firul trebuie să apeleze unlock() de același număr de ori ca lock(). Acest lucru este important pentru apeluri recursive și secțiuni critice imbricate.
Să considerăm o sarcină tipică — protejarea unui contor partajat împotriva Race Condition cu ajutorul ReentrantLock (Mutex clasic în Java/Kotlin). Fără Mutex codul ar da un rezultat incorect; cu Mutex toate cele 1000 de fire garantează creșterea valorii contorului.
import java.util.concurrent.locks.ReentrantLock
class MutexCounter {
private val mutex = ReentrantLock()
private var count = 0
fun increment() {
mutex.lock()
try {
count++ // secțiune critică
} finally {
mutex.unlock() // finally obligatoriu
}
}
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()) // Întotdeauna 1000
}
Observați blocul finally — modelul obligatoriu la lucrul cu Mutex. Dacă în interiorul secțiunii critice apare o excepție, unlock() nu va fi apelat, iar Mutex rămâne blocat pentru totdeauna — aceasta duce la Deadlock. Blocul finally garantează eliberarea Mutex indiferent de rezultatul execuției secțiunii.
O abordare alternativă în Kotlin — utilizarea funcției de extensie withLock, care gestionează automat lock/unlock cu finally.
fun increment() {
mutex.withLock { // lock + try/finally automat
count++
}
}
fun getCount(): Int = mutex.withLock { count }
Aceste trei mecanisme de sincronizare sunt adesea confundate, deși au proprietăți și domenii de aplicare diferite. Mutex — binar, cu posesie. Semaphore — contor de permisiuni, fără posesie. Monitor — mecanism de nivel înalt care combină Mutex cu variabile condiționale (condition variables). Înțelegerea diferențelor este critică pentru alegerea instrumentului potrivit pentru o sarcină specifică.
| Parametru | Mutex | Semaphore | Monitor |
|---|---|---|---|
| Tip | Binar (0/1) | Numărabil (0..N) | Binar + condiții |
| Posesie | Doar proprietarul poate unlock | Orice fir poate signal | Doar proprietarul |
| Recursivitate | De obicei da (reentrant) | Nu | Da |
| Așteptare condițională | Nu (necesită Condition) | Nu | Încorporată (wait/notify) |
| Exemplu în Java/Kotlin | ReentrantLock | Semaphore(permits) | synchronized |
Când să alegi Mutex: trebuie protejată o resursă împotriva accesului simultan — de exemplu, o colecție partajată, un fișier sau un contor. Când să alegi Semaphore — trebuie limitat numărul de accesări simultane la un pool de resurse, de exemplu un pool de conexiuni la baza de date cu 5 conexiuni. Când să alegi Monitor — este necesară sincronizarea cu așteptare condițională, de exemplu o coadă producător-consumator prin wait/notify. În dezvoltarea Android modernă, synchronized este adesea înlocuit cu ReentrantLock sau kotlinx.coroutines Mutex.
Cea mai frecventă eroare — absența blocului finally pentru apelarea unlock(). Dacă în secțiunea critică apare o excepție, Mutex rămâne blocat, iar celelalte fire așteaptă la nesfârșit. Chiar dacă ești sigur că excepțiile sunt imposibile — folosește întotdeauna try/finally sau withLock. Acesta este principiul defensive programming, deosebit de important în dezvoltarea mobilă, unde excepțiile pot apărea din cauza lipsei de memorie sau a Configuration Changes.
Când în aplicație sunt utilizate mai multe Mutex, este critic să se stabilească o ordine unică de preluare a acestora. Dacă Firul A preia M1 → M2, iar Firul B preia M2 → M1, apare Deadlock. În proiecte mari (peste 50 de mii de linii de cod), ordinea blocajelor este documentată în decizia arhitecturală și verificată de lintere. Instrumentul Lock Checker din IntelliJ IDEA detectează automat ordinea inconsecventă de preluare a blocajelor.
Menținerea Mutex mai mult de 1-2 milisecunde — un semn de design incorect. Secțiunea critică trebuie să conțină doar operațiile minim necesare. Solicitările de rețea, I/O-ul de fișiere și calculele complexe trebuie executate în afara blocului blocat. În Android, menținerea îndelungată a blocajului în firul UI duce la omiterea cadrelor (jank) și ANR. Folosește ReadWriteLock dacă secțiunea critică constă în principal din operații de citire.
Biblioteca kotlinx.coroutines oferă propria implementare a Mutex, care diferă fundamental de ReentrantLock clasic. Diferența principală — suspending Mutex nu blochează firul SO, ci suspendă corutina până la eliberarea blocajului. Aceasta înseamnă că firul poate executa alte corutine în timp ce cea curentă așteaptă 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 — nu blochează firul
count++
}
}
suspend fun getCount(): Int = mutex.withLock { count }
}
Caracteristicile cheie ale kotlinx Mutex: non-reentrant — spre deosebire de ReentrantLock, o corutină nu poate prelua din nou Mutex pe care îl deține deja. Dacă este necesar, folosește Semaphore(1) în loc de Mutex. În plus, Mutex din kotlinx.coroutines este neblocant: utilizează suspendarea prin suspend, ceea ce permite să nu blocheze firul pool-ului.
În practică, suspending Mutex este preferabil ReentrantLock clasic în codul cu corutine din două motive: scalare — o corutină așteaptă Mutex, iar firul deservește alte corutine, ceea ce crește debitul sistemului; absența BlockedThread — nu se consumă resurse pentru stocarea stivei firului blocat. Conform JetBrains (Kotlin Coroutines Guide, 2024), utilizarea suspending Mutex crește debitul cu 40% la 100+ corutine.
Întrebări frecvente
Posesia (ownership) — diferența fundamentală. Mutex își amintește care fir l-a preluat și doar acest fir îl poate elibera. Semaforul binar (Semaphore(1)) nu are proprietar — orice fir poate executa release(). Prin urmare, Mutex este mai sigur: un alt fir nu poate elibera accidental blocajul altcuiva, pe când semaforul poate.
synchronized este mai simplu și mai scurt — folosește-l pentru secțiuni critice simple fără timeout și fără control al corectitudinii. ReentrantLock folosește atunci când ai nevoie de TryLock cu timeout, planificare fair, Condition Variables sau întreruperea firului în așteptare (lockInterruptibly). Pentru corutine, folosește întotdeauna kotlinx.coroutines.sync.Mutex.
Spinlock — este un blocaj în care firul nu adoarme, ci într-o buclă (spin) verifică starea blocajului. Spinlock consumă CPU, dar nu comută contextul, ceea ce îl face avantajos pentru secțiuni critice scurte (până la 10 instrucțiuni). Mutex trece firul în starea BLOCKED, ceea ce este mai costisitor cu 10-50 de microsecunde din cauza comutării de context, dar nu consumă CPU.
La nivelul nucleului Linux, Mutex este implementat prin futex (fast userspace mutex). Firul încearcă mai întâi să preia blocajul în userspace printr-o instrucțiune atomică CAS (Compare-And-Swap). Dacă Mutex este liber — preluarea are loc fără syscall. Dacă este ocupat — firul face syscall-ul futex(FUTEX_WAIT) și adoarme. La eliberare, syscall-ul futex(FUTEX_WAKE) trezește un fir în așteptare.
Da, există Mutex inter-proces (inter-process mutex). În Windows este Named Mutex, în Linux — pthread_mutexattr_setpshared cu atributul PTHREAD_PROCESS_SHARED. În Android, Bionic libc suportă, de asemenea, Mutex inter-proces prin descriptorii de fișiere. Mutexurile inter-proces sunt utilizate pentru sincronizarea între diferite aplicații sau între un proces și procesele sale copil.
Concluzii
Vom dezvolta o aplicație mobilă la cheie
IT Sectr creează aplicații iOS și Android pentru startup-uri și afaceri din 2017. Vă vom consilia și vă vom propune cea mai bună soluție.
Citiți și