1. Einführung: Das Philosophenproblem und Coffmans vier Bedingungen (1971)
1965 formulierte der niederländische Informatiker Edsger W. Dijkstra das berühmte Problem der speisenden Philosophen (Dining Philosophers Problem): Fünf Philosophen \(P_0 \dots P_4\) sitzen an einem runden Tisch; zwischen je zwei Tellern liegt eine einzelne Gabel (\(F_0 \dots F_4\)). Zum Essen benötigt jeder Philosoph gleichzeitig seine linke Gabel \(F_i\) und seine rechte Gabel \(F_{(i+1)\bmod 5}\). Greifen alle fünf Philosophen im exakt selben Moment nach ihrer linken Gabel \(F_i\), hält jeder eine Gabel fest und wartet bis in alle Ewigkeit darauf, dass sein rechter Nachbar die zweite Gabel freigibt – das gesamte System steht still. Dieser Zustand heißt Deadlock (Verklemmung).
Formal befindet sich eine nichtleere Prozessmenge \(D \subseteq \mathcal{P}\) im Deadlock, wenn jeder Prozess \(P_i \in D\) dauerhaft auf ein Ereignis (wie die Freigabe einer Ressource) wartet, das ausschließlich von einem anderen Prozess \(P_j \in D\) derselben Wartemenge ausgelöst werden kann.
1971 bewiesen Edward G. Coffman Jr., M. J. Elphick und Arie Shoshani, dass ein Ressourcen-Deadlock nur eintreten kann, wenn vier notwendige Bedingungen gleichzeitig erfüllt sind:
- Wechselseitiger Ausschluss (Mutual Exclusion): Mindestens eine Ressource wird exklusiv (nicht teilbar) von einem Prozess belegt.
- Halten und Warten (Hold and Wait): Ein Prozess hält bereits mindestens eine Ressource, während er blockiert auf weitere, von anderen Prozessen gehaltene Ressourcen wartet.
- Keine Präemption (No Preemption): Zugeteilte Ressourcen können einem Prozess nicht gewaltsam entzogen werden, sondern werden nur freiwillig freigegeben.
- Zyklisches Warten (Circular Wait): Es existiert eine geschlossene Kette wartender Prozesse \(\{P_0, P_1, \dots, P_{k-1}\}\), sodass jeder \(P_i\) auf eine von \(P_{(i+1)\bmod k}\) gehaltene Ressource wartet.
Da alle vier Bedingungen einzeln notwendig sind, gilt: Wird auch nur eine einzige dieser vier Bedingungen durch das Systemdesign logisch ausgeschlossen, ist ein Deadlock mathematisch zu \(100\,\%\) unmöglich.
2. Ressourcenallokationsgraph (RAG) und die Zyklensätze (Holt, 1972)
1972 modellierte Richard C. Holt Ressourcenanforderungen als gerichteten bipartiten Ressourcenallokationsgraphen (RAG) \(G = (V, E)\) mit Knotenmenge \(V = \mathcal{P} \cup \mathcal{R}\) (Prozessknoten \(P_i\) und Ressourcentypknoten \(R_j\) mit je \(W_j \ge 1\) identischen Instanzen) sowie Anforderungskanten \(P_i \to R_j\) und Zuweisungskanten \(R_j \to P_i\).
Zwischen gerichteten Zyklen in \(G\) und Deadlocks gelten drei fundamentale Sätze:
- Satz 1 (Zyklenfreiheit impliziert Deadlock-Freiheit): Enthält \(G\) keinen gerichteten Zyklus (ist \(G\) ein DAG), befindet sich kein Prozess im Deadlock.
- Satz 2 (Notwendige und hinreichende Bedingung bei Einzelinstanzen): Besitzt jeder Ressourcentyp genau eine Instanz (\(\forall j, W_j = 1\)), so gilt: Gerichteter Zyklus in \(G \iff\) Deadlock.
- Satz 3 (Nicht-Hinreichendheit bei Mehrfachinstanzen): Besitzt mindestens ein Ressourcentyp mehrere Instanzen (\(\exists j, W_j > 1\)), ist ein gerichteter Zyklus zwar eine notwendige, aber KEINE hinreichende Bedingung für einen Deadlock!
flowchart TD
subgraph RAG_Single["Fall A: Einzelinstanz-RAG (Zyklus <==> Deadlock garantiert)"]
PA1(("P1 (Blockiert)")) -->|"Fordert an"| RA1["R1 (1 Instanz: ●)"]
RA1 -->|"Zugewiesen"| PA2(("P2 (Blockiert)"))
PA2 -->|"Fordert an"| RA2["R2 (1 Instanz: ●)"]
RA2 -->|"Zugewiesen"| PA1
end
subgraph RAG_Multi["Fall B: Mehrfachinstanz-RAG (Zyklus P1->R1->P3->R2->P1, aber KEIN Deadlock!)"]
PB1(("P1 (Blockiert)")) -->|"Fordert an"| RB1["R1 (2 Instanzen: ●●)"]
RB1 -->|"Instanz #1"| PB3(("P3 (Blockiert)"))
PB3 -->|"Fordert an"| RB2["R2 (2 Instanzen: ●●)"]
RB2 -->|"Instanz #1"| PB1
RB1 -->|"Instanz #2"| PB2(("P2 (LÄUFT: Keine Anforderung!)"))
RB2 -->|"Instanz #2"| PB4(("P4 (LÄUFT: Keine Anforderung!)"))
end
In Fall B existiert zwar der Zyklus \(P_1 \to R_1 \to P_3 \to R_2 \to P_1\), doch die Prozesse \(P_2\) und \(P_4\) haben Ausgangsgrad \(0\) (Senken), laufen ungehindert weiter und lösen den Zyklus durch Freigabe ihrer Instanzen von \(R_1\) bzw. \(R_2\) auf!
3. Strategie 1 (Deadlock-Prävention): Widerspruchsbeweis der totalen Sperrordnung
In Betriebssystemkernen (z. B. Linux lockdep) und Multithreading-Anwendungen wird Bedingung 4 (Zyklisches Warten) durch eine totale Sperrordnung (Total Lock Ordering) eliminiert: Jedem Mutex \(L \in \mathcal{L}\) wird ein eindeutiger globaler Rang \(\phi(L) \in \mathbb{N}\) zugewiesen. Ein Thread, der bereits die Sperrmenge \(\mathcal{H}\) hält, darf eine neue Sperre \(L_{\text{new}}\) nur anfordern, wenn ihr Rang strikt größer ist:
【Widerspruchsbeweis】: Angenommen, es entsteht ein zyklischer Wartekreis \(\{P_0, P_1, \dots, P_{k-1}\}\) (\(k \ge 2\)), wobei \(P_i\) die Sperre \(L_i\) hält und auf die von \(P_{(i+1)\bmod k}\) gehaltene Sperre \(L_{(i+1)\bmod k}\) wartet. Da jeder Thread die Sperrordnung befolgt, gilt entlang des gesamten Zyklus:
\[\phi(L_0) < \phi(L_1) < \phi(L_2) < \cdots < \phi(L_{k-1}) < \phi(L_0) \implies \phi(L_0) < \phi(L_0)\]Da keine natürliche Zahl strikt kleiner als sie selbst sein kann, liegt ein Widerspruch (\(\bot\)) vor! Ob beim Banktransfer (immer das Konto mit der kleineren ID zuerst sperren) oder bei den speisenden Philosophen (\(P_4\) greift zuerst zur rechten Gabel \(F_0\) statt \(F_4\), da \(0 < 4\)) – zyklisches Warten ist damit mathematisch ausgeschlossen. \(\blacksquare\)
4. Strategie 2 (Deadlock-Vermeidung): Dijkstras Bankieralgorithmus (5×3-Matrix-Trace)
Lässt sich die Sperrreihenfolge nicht statisch festlegen, fordert Dijkstras Bankieralgorithmus (1965 / Habermann, 1969) vorab die Deklaration des Maximalbedarfs \(\mathbf{Max}\) und prüft vor jeder Zuteilung in \(O(m \cdot n^2)\) Schritten, ob der Zielzustand ein sicherer Zustand (Safe State) mit mindestens einer sicheren Ausführungsfolge (Safe Sequence) bleibt (wobei gilt: \(\text{Deadlock} \subsetneq \text{Unsicherer Zustand}\)).
Für \(n=5\) Prozesse \(\{P_0 \dots P_4\}\) und \(m=3\) Ressourcentypen mit \(\mathbf{Total} = (A=10, B=5, C=7)\) ergibt sich zum Zeitpunkt \(t_0\) bei belegten Ressourcen \((7,2,5)\) der freie Vektor \(\mathbf{Available} = (10,5,7) - (7,2,5) = \mathbf{(3, 3, 2)}\):
| Prozess | Belegt \(\mathbf{Allocation}\) \((A,B,C)\) | Maximal \(\mathbf{Max}\) \((A,B,C)\) | Restbedarf \(\mathbf{Need} = \mathbf{Max} - \mathbf{Allocation}\) | Sicherheitsprüfung (Start \(\mathbf{Work} = (3,3,2)\)) |
|---|---|---|---|---|
| \(P_0\) | \((0, 1, 0)\) | \((7, 5, 3)\) | \((7, 4, 3)\) | Schritt 4 erfüllt: \((7,4,3) \le (7,4,5) \to \mathbf{Work}=(7,5,5)\) |
| \(P_1\) | \((2, 0, 0)\) | \((3, 2, 2)\) | \((1, 2, 2)\) | Schritt 1 erfüllt: \((1,2,2) \le (3,3,2) \to \mathbf{Work}=(5,3,2)\) |
| \(P_2\) | \((3, 0, 2)\) | \((9, 0, 2)\) | \((6, 0, 0)\) | Schritt 5 erfüllt: \((6,0,0) \le (7,5,5) \to \mathbf{Work}=(10,5,7)\) |
| \(P_3\) | \((2, 1, 1)\) | \((2, 2, 2)\) | \((0, 1, 1)\) | Schritt 2 erfüllt: \((0,1,1) \le (5,3,2) \to \mathbf{Work}=(7,4,3)\) |
| \(P_4\) | \((0, 0, 2)\) | \((4, 3, 3)\) | \((4, 3, 1)\) | Schritt 3 erfüllt: \((4,3,1) \le (7,4,3) \to \mathbf{Work}=(7,4,5)\) |
Mit der sicheren Folge \(\langle P_1, P_3, P_4, P_0, P_2 \rangle\) ist \(t_0\) sicher. Fordert \(P_1\) nun \((1,0,2)\) an, bleibt \(\mathbf{Available}=(2,3,0) \ge \mathbf{Need}_1=(0,2,0)\) sicher (sofort genehmigt). Fordert danach jedoch \(P_0\) den Vektor \((0,2,0)\) an, sinkt \(\mathbf{Available}\) auf \((2,1,0)\), womit kein einziger Prozess mehr seinen \(\mathbf{Need}\) decken kann (unsicherer Zustand): Der Bankieralgorithmus lehnt die Zuteilung ab und blockiert \(P_0\)!
5. Strategie 3 & 4 (Erkennung, Wait-Die/Wound-Wait, Vogel-Strauß) & Livelock vs. Starvation
Relationale Datenbanken (PostgreSQL, MySQL InnoDB) nutzen einen Wait-For-Graphen (WFG) und suchen mittels \(O(|V|+|E|)\)-Tiefensuche (DFS) nach Zyklen, um bei einem Deadlock die Transaktion mit dem kleinsten Undo-Log als Opfer (Victim) zurückzurollen (MySQL prüft sofort beim Blockieren; PostgreSQL erst nach deadlock_timeout = 1s). Verteilte Datenbanken (wie Google Spanner) vermeiden globale WFGs durch die zeitstempelbasierten Protokolle Wait-Die (nicht-präemptiv) und Wound-Wait (präemptiv, weniger Neustarts) nach Rosenkrantz et al. (1978), wobei abgebrochene Transaktionen ihren ursprünglichen Startzeitstempel \(\text{TS}(T_i)\) behalten, um Verhungern (Starvation) auszuschließen. Für extrem seltene Betriebssystem-Tabellenengpässe nutzen UNIX/Windows den Vogel-Strauß-Algorithmus (Ostrich Algorithm) und ignorieren das Problem zugunsten maximaler Alltagsgeschwindigkeit.
flowchart TD
Req["Verteilte Transaktion Ti fordert eine von Tj gehaltene Sperre an"] --> Cmp{"Startzeitstempel-Vergleich: TS(Ti) < TS(Tj) ?<br/>(Ist Anforderer Ti ÄLTER als Halter Tj?)"}
Cmp -->|"Ja: Ti ist ÄLTER (Older)"| WD1["[Wait-Die (nicht-präemptiv)]: Älteres Ti WARTET auf jüngeres Tj (Alt -> Jung)<br/>[Wound-Wait (präemptiv)]: Älteres Ti VERWUNDET (bricht ab) Tj & übernimmt Sperre!"]
Cmp -->|"Nein: Ti ist JÜNGER (Younger)"| WD2["[Wait-Die (nicht-präemptiv)]: Jüngeres Ti STIRBT (Abbruch, Retry mit altem TS)<br/>[Wound-Wait (präemptiv)]: Jüngeres Ti WARTET geduldig auf älteres Tj (Jung -> Alt)"]
Abschließend die präzise Abgrenzung der drei Liveness-Fehler:
| Diagnose-Dimension | Deadlock (Verklemmung) | Livelock | Starvation (Verhungern) |
|---|---|---|---|
| Kernmechanismus | Zyklisches Warten blockierter Threads auf gehaltene Ressourcen | Symmetrische trylock-Retry-Schleifen machen sich gegenseitig den Fortschritt zunichte (Flur-Ausweichtanz) | Gesamtsystem schreitet voran, aber ein Thread wird durch Prioritäten dauerhaft übergangen |
| OS-Status & CPU-Last | SLEEPING / BLOCKED (\(0\,\%\) CPU-Last) | RUNNING (\(\approx 100\,\%\) CPU-Last bei \(0\) Fortschritt!) | Wartend in Ready-/Lock-Queue (\(0\,\%\) CPU für das Opfer) |
| Technische Heilung | Totale Sperrordnung (\(\phi(L_1)<\phi(L_2)\)), Bankieralgorithmus, WFG-Rollback | Randomisiertes Exponentielles Backoff mit Jitter bricht die Zeitsymmetrie | Priority Aging, Beibehalten des Start-Zeitstempels, faire FIFO-Locks (MCS-Lock) |
