Mutex (vzájemné vyloučení) — je synchronizační primitivum, které zaručuje, že pouze jedno vlákno může vykonávat kritickou sekci kódu v každém okamžiku. Podle Microsoft Docs (Synchronization Objects, 2024) je základním principem Mutex vlastnictví: vlákno, které Mutex získalo, se stává jeho vlastníkem a uvolňuje jej až při opuštění kritické sekce. Mutex — základní nástroj pro prevenci Race Condition a zajištění integrity dat ve vícevláknových aplikacích.
Hlavní body
Mutex (zkratka z Mutual Exclusion — vzájemné vyloučení) — je synchronizační objekt, který řídí přístup ke sdílenému zdroji ve vícevláknovém prostředí. Když vlákno vstoupí do kritické sekce, získá Mutex. Pokud se jiné vlákno pokusí získat stejný Mutex, je převedeno do stavu čekání, dokud není zámek uvolněn prvním vláknem.
Architektura Mutex sahá k operačnímu systému THE, vyvinutému Edsgerem Dijkstrou v roce 1965. Právě Dijkstra zavedl koncept semaforů, z nichž se později Mutex vyčlenil jako zvláštní případ — binární semafor s podporou vlastnictví. Moderní operační systémy (Linux, Windows, Android) implementují Mutex na úrovni jádra, což zajišťuje správnou synchronizaci i mezi různými procesy.
Klíčovou vlastností Mutex je ownership (vlastnictví). Pouze vlákno, které mutex získalo, jej může uvolnit. To odlišuje Mutex od binárního semaforu, kde jakékoli vlákno může provést signál (V-operaci). Vlastnictví zabraňuje náhodnému uvolnění zámku jiným vláknem, což činí Mutex bezpečnějším pro typické scénáře synchronizace v mobilním vývoji. Podle Android Developer Docs (Processes and Threads, 2024) může použití Mutex místo synchronized zvýšit výkon o 30% při vysoké konkurenci.
Mutex je v jednom ze dvou stavů: zamčený (locked) — získaný vláknem; volný (unlocked) — nezískaný. Dvě základní operace — lock() (získání) a unlock() (uvolnění). Pokud je Mutex již získán, vlákno volající lock() je blokováno až do uvolnění. V JVM přechází blokované vlákno do stavu BLOCKED a nespotřebovává CPU.
Když je Mutex uvolněn, systém vybírá, které z čekajících vláken získá zámek. Při nespravedlivém (non-fair) plánování může volba padnout na vlákno, které právě uvolnilo mutex — to zvyšuje propustnost, ale může vést k Starvation (hladovění). Spravedlivý (fair) plánovač používá frontu FIFO: první čekající vlákno získá zámek jako první. ReentrantLock(true) implementuje právě tento mechanismus.
Většina implementací Mutex v Java/Kotlin podporuje rekurzivní (reentrant) získávání. Pokud vlákno již Mutex vlastní a znovu zavolá lock(), operace je úspěšná — Mutex neblokuje sám sebe. Počítadlo rekurze se zvyšuje a vlákno musí zavolat unlock() tolikrát, kolikrát lock(). To je důležité pro rekurzivní volání a vnořené kritické sekce.
Uvažujme typický úkol — ochranu sdíleného čítače před Race Condition pomocí ReentrantLock (klasický Mutex v Java/Kotlin). Bez Mutex by kód dával nesprávný výsledek; s Mutex všech 1000 vláken zaručeně zvyšuje hodnotu čítače.
import java.util.concurrent.locks.ReentrantLock
class MutexCounter {
private val mutex = ReentrantLock()
private var count = 0
fun increment() {
mutex.lock()
try {
count++ // kritická sekce
} finally {
mutex.unlock() // povinný 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()) // Vždy 1000
}
Všimněte si bloku finally — povinného vzoru při práci s Mutex. Pokud v kritické sekci dojde k výjimce, unlock() se nevyvolá a Mutex zůstane navždy zamčený — to vede k Deadlock. Blok finally zaručuje uvolnění Mutex při jakémkoli výsledku provedení sekce.
Alternativní přístup v Kotlin — použití rozšiřující funkce withLock, která automaticky zpracovává lock/unlock s finally.
fun increment() {
mutex.withLock { // lock + try/finally automaticky
count++
}
}
fun getCount(): Int = mutex.withLock { count }
Tyto tři synchronizační mechanismy jsou často zaměňovány, ačkoli mají různé vlastnosti a oblasti použití. Mutex — binární, s vlastnictvím. Semaphore — čítač povolení, bez vlastnictví. Monitor — mechanismus vysoké úrovně kombinující Mutex s podmínkovými proměnnými (condition variables). Porozumění rozdílům je kritické pro výběr správného nástroje pro konkrétní úkol.
| Parametr | Mutex | Semaphore | Monitor |
|---|---|---|---|
| Typ | Binární (0/1) | Počitatelný (0..N) | Binární + podmínky |
| Vlastnictví | Pouze vlastník může unlock | Jakékoli vlákno může signal | Pouze vlastník |
| Rekurzivita | Obvykle ano (reentrant) | Ne | Ano |
| Podmíněné čekání | Ne (je třeba Condition) | Ne | Vestavěné (wait/notify) |
| Příklad v Java/Kotlin | ReentrantLock | Semaphore(permits) | synchronized |
Kdy zvolit Mutex: je třeba chránit jeden zdroj před současným přístupem — například sdílenou kolekci, soubor nebo čítač. Kdy zvolit Semaphore — je třeba omezit počet současných přístupů do fondu zdrojů, například fond databázových spojení s 5 připojeními. Kdy zvolit Monitor — je třeba synchronizace s podmíněným čekáním, například fronta producer-consumer přes wait/notify. V moderním Android vývoji je synchronized často nahrazován ReentrantLock nebo kotlinx.coroutines Mutex.
Nejčastější chyba — absence bloku finally pro volání unlock(). Pokud v kritické sekci dojde k výjimce, Mutex zůstane zamčený a ostatní vlákna čekají věčně. I když jste si jisti, že výjimky jsou nemožné — vždy používejte try/finally nebo withLock. To je princip defensive programming, obzvláště důležitý v mobilním vývoji, kde výjimky mohou vzniknout z důvodu nedostatku paměti nebo Configuration Changes.
Když je v aplikaci používáno více Mutex, je kritické stanovit jednotné pořadí jejich získávání. Pokud Vlákno A získává M1 → M2 a Vlákno B získává M2 → M1, vzniká Deadlock. Ve velkých projektech (přes 50 tisíc řádků kódu) je pořadí zámků dokumentováno v architektonickém rozhodnutí a kontrolováno lintery. Nástroj Lock Checker v IntelliJ IDEA automaticky detekuje nekonzistentní pořadí získávání zámků.
Držení Mutex déle než 1-2 milisekundy — známka nesprávného návrhu. Kritická sekce by měla obsahovat pouze minimálně nutné operace. Síťové požadavky, souborový vstup/výstup a složité výpočty by měly být prováděny mimo uzamčený blok. V Androidu vede dlouhé držení zámku ve vláknu UI k přeskakování snímků (jank) a ANR. Použijte ReadWriteLock, pokud kritická sekce sestává převážně z operací čtení.
Knihovna kotlinx.coroutines poskytuje vlastní implementaci Mutex, která se zásadně liší od klasického ReentrantLock. Hlavní rozdíl — suspending Mutex neblokuje vlákno OS, ale pozastavuje korutinu až do uvolnění zámku. To znamená, že vlákno může vykonávat jiné korutiny, zatímco aktuální čeká na 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 — neblokuje vlákno
count++
}
}
suspend fun getCount(): Int = mutex.withLock { count }
}
Klíčové vlastnosti kotlinx Mutex: nerekurzivnost (non-reentrant) — na rozdíl od ReentrantLock, korutina nemůže znovu získat Mutex, který již vlastní. Pokud je to nutné, použijte Semaphore(1) místo Mutex. Kromě toho je Mutex z kotlinx.coroutines neblokující: používá pozastavení pomocí suspend, což umožňuje neblokovat vlákno fondu.
V praxi je suspending Mutex preferován před klasickým ReentrantLock v korutinovém kódu ze dvou důvodů: škálovatelnost — jedna korutina čeká na Mutex, zatímco vlákno obsluhuje jiné korutiny, což zvyšuje propustnost systému; absence BlockedThread — neplýtvá se prostředky na ukládání zásobníku blokovaného vlákna. Podle JetBrains (Kotlin Coroutines Guide, 2024) zvyšuje použití suspending Mutex propustnost o 40% při 100+ korutinách.
Často kladené otázky
Vlastnictví (ownership) — zásadní rozdíl. Mutex si pamatuje, které vlákno jej získalo, a pouze toto vlákno jej může uvolnit. Binární semafor (Semaphore(1)) nemá vlastníka — jakékoli vlákno může provést release(). Proto je Mutex bezpečnější: jiné vlákno nemůže náhodně uvolnit cizí zámek, zatímco semafor může.
synchronized je jednodušší a kratší — použijte jej pro jednoduché kritické sekce bez časových limitů a bez kontroly spravedlnosti. ReentrantLock použijte, když potřebujete TryLock s timeoutem, fair-plánování, Condition Variables nebo přerušení čekajícího vlákna (lockInterruptibly). Pro korutiny vždy používejte kotlinx.coroutines.sync.Mutex.
Spinlock — je zámek, při kterém vlákno nespí, ale ve smyčce (spin) kontroluje stav zámku. Spinlock spotřebovává CPU, ale nepřepíná kontext, což jej činí výhodným pro krátké kritické sekce (do 10 instrukcí). Mutex převádí vlákno do stavu BLOCKED, což je o 10-50 mikrosekund dražší kvůli přepnutí kontextu, ale nespotřebovává CPU.
Na úrovni jádra Linux je Mutex implementován prostřednictvím futex (fast userspace mutex). Vlákno se nejprve pokusí získat zámek v userspace pomocí atomické instrukce CAS (Compare-And-Swap). Pokud je Mutex volný — získání proběhne bez syscall. Pokud je obsazený — vlákno provede syscall futex(FUTEX_WAIT) a usne. Při uvolnění syscall futex(FUTEX_WAKE) probudí jedno čekající vlákno.
Ano, existují meziprocesové Mutex (inter-process mutex). V Windows je to Named Mutex, v Linux — pthread_mutexattr_setpshared s atributem PTHREAD_PROCESS_SHARED. V Android Bionic libc také podporuje meziprocesové Mutex přes file deskriptory. Meziprocesové Mutex se používají pro synchronizaci mezi různými aplikacemi nebo mezi procesem a jeho podřízenými procesy.
Shrnutí
Vyvineme mobilní aplikaci na klíč
IT Sectr vytváří aplikace pro iOS a Android pro startupy a podniky od roku 2017. Poradíme vám a navrhneme nejlepší řešení.
Přečtěte si také