Deadlock (vzájemné blokování) — je stav, ve kterém dvě nebo více vláken nekonečně čekají na uvolnění zdrojů obsazených jinými účastníky. Podle Oracle Java Tutorials (2024), Deadlock vzniká při kruhovém čekání, když každé vlákno drží zámek potřebný jinému vláknu. Bez speciálních prostředků detekce Deadlock úplně zastaví provádění aplikace bez viditelných chyb.
Hlavní body
Deadlock (vzájemné blokování) — je situace v multithreadingovém programování, kde dvě nebo více vláken trvale blokují navzájem. Každé vlákno drží zdroj potřebný jinému vláknu a neuvolňuje jej, čekající na získání chybějícího zdroje. Výsledkem je, že žádné z vláken nemůže pokračovat v provádění.
V mobilním vývoji je Deadlock obzvláště kritický, protože nezpůsobuje výjimky nebo pády. Aplikace jednoduše přestane reagovat na akce uživatele (ANR — Application Not Responding), a jediným východiskem je nucené ukončení procesu. Podle údajů Google (Android Performance Patterns, 2023) asi 15% hlášení ANR v Google Play Console souvisí se vzájemnými blokováními na pozadí.
Klíčový rozdíl Deadlocku od jiných problémů konkurence — jeho nevratnost bez vnějšího zásahu. Vlákna neuvolní zdroje sama, protože plánovač operačního systému nemůže nuceně odebrat zámek. To odlišuje Deadlock od Livelocku, kde jsou vlákna aktivní, ale nevykonávají užitečnou práci.
V roce 1971 Edward G. Coffman formuloval čtyři povinné podmínky nutné pro vznik Deadlocku. Pokud alespoň jedna chybí, vzájemné blokování je nemožné. Tyto podmínky jsou známy jako Coffmanovy podmínky a tvoří základ všech algoritmů prevence Deadlocku.
Zdroj může být získán pouze jedním vláknem v každém okamžiku. Pokud zdroj umožňuje současné čtení více vlákny (například ReadWriteLock v režimu čtení), Deadlock nevzniká. Tato podmínka vyplývá z podstaty Mutexu a zámků.
Vlákno drží již získaný zdroj a současně čeká na získání jiného zdroje. Pokud vlákno může uvolnit aktuální zdroj před požadavkem na další (prostřednictvím dvoufázového zamykání), podmínka Hold and Wait je porušena. V Androidu se to často projevuje, když vlákno drží zámek databáze a snaží se získat zámek SharedPreferences.
Operační systém nemůže nuceně odebrat zámek vláknu. Zdroj je uvolněn pouze tehdy, když jej vlákno samo uvolní. V některých systémech (například SQLite WAL režim) je nucené převzetí implementováno na úrovni jednotlivých operací, což snižuje riziko Deadlocku.
Existuje uzavřený řetězec vláken, z nichž každé čeká na zdroj držený následujícím v řetězci. Například, vlákno A drží zdroj 1 a čeká na zdroj 2, vlákno B drží zdroj 2 a čeká na zdroj 1. Toto je jediná podmínka, kterou může vývojář architektonicky odstranit — pomocí hierarchie zámků. Pokud všechna vlákna získávají zdroje v přísně stanoveném globálním pořadí, cyklus je fyzicky nemožný.
V praxi v aplikacích pro Android Deadlock nejčastěji vzniká kvůli implicitnímu křížení zámků různých úrovní: zámku databáze (Room), zámku SharedPreferences a zámku kolekce v paměti. Každý z těchto zámků je řízen různými komponentami a bez centralizovaného protokolu pořadí získávání vývojáři neúmyslně vytvářejí cykly.
Podívejme se na klasický příklad vzájemného blokování — dvě vlákna získávají zámky v různém pořadí. Pokud první vlákno blokuje zdroj A a snaží se získat B, a druhé blokuje B a snaží se získat A, vzniká Deadlock.
class DeadlockExample {
private val lockA = Any()
private val lockB = Any()
fun operationA() {
synchronized(lockA) {
Thread.sleep(50) // simulace práce
synchronized(lockB) {
println("operationA provedena")
}
}
}
fun operationB() {
synchronized(lockB) { // obrácené pořadí!
Thread.sleep(50)
synchronized(lockA) {
println("operationB provedena")
}
}
}
}
fun main() {
val ex = DeadlockExample()
Thread { ex.operationA() }.start()
Thread { ex.operationB() }.start()
// Aplikace navždy zamrzne — Deadlock!
}
V tomto příkladě operationA získává lockA a operationB získává lockB. Poté se každé snaží získat druhý zámek — a obě nekonečně čekají. Program zamrzne bez chybové zprávy. Jediným způsobem nápravy je zaručit stejné pořadí získávání zámků ve všech metodách.
Tyto tři problémy konkurence jsou často zaměňovány, ale jejich mechanismy a důsledky jsou zásadně odlišné. Deadlock — úplné zastavení, Starvation — nekonečné čekání na zdroj, Livelock — aktivní nečinnost. Pochopení rozdílů je kritické pro výběr správné strategie odstranění.
| Charakteristika | Deadlock | Starvation | Livelock |
|---|---|---|---|
| Stav vláken | Blokována (BLOCKED) | Připravena (RUNNABLE) | Aktivní (RUNNABLE) |
| Provádění práce | Ne | Ne | Ano, ale neužitečná |
| Příčina | Kruhové čekání | Nespravedlivé plánování | Nesprávné řešení konfliktu |
| Detekce | Thread Dump, časové limity | Sledování pokroku | PoĄet opakování |
Starvation (hladovění) vzniká, když plánovač neustále odkládá provedení nízkoprioritního vlákna ve prospěch jiných. Na rozdíl od Deadlocku vlákno není blokováno — je připraveno k provedení, ale nezískává čas CPU. V Androidu je typickým scénářem nízkoprioritní vlákno na pozadí, které se nikdy neprovede, pokud jsou vlákna UI a Service neustále aktivní.
Livelock (aktivní blokování) — situace, kdy vlákna nejsou blokována, ale nekonečně reagují na vzájemné akce, aniž by vykonávala užitečnou práci. Klasická analogie — dva lidé se potkají na chodbě a oba se snaží uhnout z cesty, přičemž se pohybují stejným směrem. Na rozdíl od Deadlocku vlákna v Livelocku spotřebovávají CPU, čímž vybíjejí baterii zařízení.
Thread Dump (výdpis vláken) — hlavní nástroj detekce vzájemných blokování v JVM a Android Runtime. Při výdpisu JVM automaticky analyzuje graf závislostí mezi monitory a označuje cykly Deadlocku. V Android Studio lze výdpis vláken získat prostřednictvím Android Profiler nebo příkazem kill -3 PID z ADB Shell.
Automatická detekce Deadlocku během provádění je realizována pomocí Watchdog časovačů. Pokud vlákno nedokončí operaci ve stanoveném časovém limitu, watchdog zahájí vytvoření výdpisu a odešle zprávu do systému hlášení chyb (Firebase Crashlytics, Sentry). Podle údajů Sentry (Issue Resolution Report, 2024) konfigurace watchdogu zkracuje dobu diagnostiky Deadlocku z týdnů na několik hodin.
Ve fázi vývoje jsou účinné statický analyzátor ThreadSafe od JetBrains a Checker Framework s modulem Lock Checker. Tyto nástroje analyzují pořadí získávání zámků na úrovni zdrojového kódu a varují před potenciálními cykly. Doplňkově se doporučuje Test-Driven Deadlock Detection — zátěžové testy, které spouštějí operace s různým pořadím zamykání ve stovkách vláken.
Zvláštní pozornost si zaslouží Cooperative Deadlock Detection — metoda, při které si vlákna vyměňují informace o získaných zámcích prostřednictvím globálního registru. Pokud vlákno detekuje potenciální cyklus, uvolní všechny zdroje a zopakuje operaci. Tento přístup se používá v distribuovaných systémech (Apache ZooKeeper, Google Chubby) a postupně se zavádí do mobilního vývoje prostřednictvím knihoven jako Jetpack Sync.
Nejspolehlivější způsob — stanovení globálního pořadí získávání zámků v celé aplikaci. Pokud všechna vlákna nejprve získají zámek s nižším číslem a poté s vyšším, kruhové čekání (podmínka Circular Wait) je nemožné. Ve velkých projektech je pořadí stanoveno v dokumentaci a ověřováno pomocí code review.
TryLock — metoda zamykání, která neblokuje vlákno nekonečně, ale vrací false, pokud zámek není získán ve stanoveném čase. V Javě je to implementováno pomocí ReentrantLock.tryLock(timeout, TimeUnit), v Kotlin Coroutines pomocí Mutex.withLock s časovým limitem. Při neúspěchu vlákno uvolní všechny získané zdroje a zkusí to později.
Bankéřův algoritmus — teoretická metoda prevence Deadlocku navržená Edsgerem Dijkstrou. Modeluje distribuci zdrojů jako bankovní transakce: systém nepřiděluje zdroj, pokud by to mohlo vést k nebezpečnému stavu (deadlock). V praxi se algoritmus v mobilním vývoji používá zřídka kvůli složitosti předběžné znalosti maximálních potřeb vláken, ale jeho principy se používají v databázích SQLite a souborových systémech.
Často kladené otázky
Ne, pro vzájemné blokování jsou potřeba alespoň dvě vlákna. V jednom vláknu jsou všechny operace prováděny sekvenčně, takže kruhové čekání je nemožné. Deadlock však může nastat mezi procesy při použití zámků souborů nebo meziprocesorových semaforů.
V korutinách Deadlock vzniká na úrovni pozastavených funkcí (suspend) a neblokuje vlákno OS, což ho činí méně patrným. Mutex z kotlinx.coroutines je pozastavující (suspending), neblokuje vlákno, ale korutina se neprovede. K detekci použijte DebugProbes z modulu kotlinx-coroutines-debug.
Deadlock v SQLite vzniká, když dvě připojení k databázi se snaží provést transakce v různém pořadí. SQLite takové situace detekuje a vrací chybový kód SQLITE_BUSY nebo SQLITE_LOCKED. V Androidu se doporučuje používat Room s jedinou instancí databáze a transakcemi přes @Transaction, což eliminuje Deadlock mezi připojeními.
Android Runtime má vestavěný detektor Deadlocku, který se spouští při generování ANR (Application Not Responding). Systém analyzuje Thread Dump všech vláken aplikace a označuje vzájemná blokování. Výsledek je k dispozici v /data/anr/traces.txt a Google Play Console v sekci ANR Reports.
Nejprve získejte Thread Dump všech vláken aplikace. Analyzujte, jaké zámky každé vlákno drží a jaké se snaží získat. Implementujte Watchdog časovač s automatickým výdpisem při překročení časového limitu. Po opravě přidejte lint pravidlo ThreadSafety do CI pipeline pro prevenci recidivy.
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é