1. Beispiel für einen Deadlock in C#
Deadlock — das ist eine Situation, in der jeder Thread aus einer Gruppe eine Ressource hält und auf die Freigabe einer anderen Ressource wartet, die von einem anderen Thread derselben Gruppe gehalten wird — und niemand bekommt, was er braucht.
Schauen wir uns das praktisch an. Angenommen, wir haben zwei Lock-Objekte und zwei Threads. Jeder Thread sperrt ein Objekt und versucht dann, das zweite zu bekommen.
using System;
using System.Threading;
class Program
{
static readonly object lockerA = new object();
static readonly object lockerB = new object();
static void Main()
{
Thread thread1 = new Thread(Thread1Work);
Thread thread2 = new Thread(Thread2Work);
thread1.Start();
thread2.Start();
thread1.Join();
thread2.Join();
Console.WriteLine("Beide Threads haben ihre Arbeit beendet (falls kein Deadlock aufgetreten ist)");
}
static void Thread1Work()
{
lock (lockerA)
{
Console.WriteLine("Thread 1: hat lockerA genommen");
Thread.Sleep(100); // Geben wir dem zweiten Thread die Chance, lockerB zu nehmen
lock (lockerB)
{
Console.WriteLine("Thread 1: hat lockerB genommen");
}
}
}
static void Thread2Work()
{
lock (lockerB)
{
Console.WriteLine("Thread 2: hat lockerB genommen");
Thread.Sleep(100); // Geben wir dem ersten Thread die Chance, lockerA zu nehmen
lock (lockerA)
{
Console.WriteLine("Thread 2: hat lockerA genommen");
}
}
}
}
Führt diesen Code ein paar Mal aus — und ihr werdet fast sicher feststellen, dass die Anwendung "hängt". Ein Deadlock ist aufgetreten! Jeder Thread hat seinen Lock und wartet nun, bis der andere den zweiten Lock freigibt.
Visualisierung des Deadlocks
So sieht das schematisch aus:
sequenceDiagram
participant Thread 1
participant Thread 2
participant lockerA
participant lockerB
Thread 1->>lockerA: lockerA nehmen
Thread 2->>lockerB: lockerB nehmen
Thread 1->>lockerB: wartet auf Freigabe von lockerB
Thread 2->>lockerA: wartet auf Freigabe von lockerA
Im Ergebnis: Thread 1 hält lockerA und wartet auf lockerB, Thread 2 hält lockerB und wartet auf lockerA. Keiner weicht — das Programm hängt.
2. Wie entdeckt man einen Deadlock?
Warum passiert das?
Um die Ursache zu verstehen, betrachten wir einige Bestandteile:
- Mehrfaches Sperren: Wenn ein Thread mehrere Locks gleichzeitig hält.
- Uneinheitliche Reihenfolge der Locks: Wenn Threads Locks in unterschiedlicher Reihenfolge nehmen, kann ein deadlock entstehen.
- Möglichkeit des Wartens: Ein Thread wartet auf die Freigabe einer Ressource, die von einem anderen Thread gehalten wird.
- Keine erzwungene Übernahme: Ein Thread kann einem anderen die Sperre in C# nicht "abziehen" — er kann nur warten.
Die klassische Falle: wenn ihr N Threads und N Ressourcen habt und die Threads die Ressourcen in unterschiedlicher Reihenfolge sperren, seid ihr nahe am Deadlock.
Typische Hinweise auf Deadlock
Deadlock sieht von außen oft wie normales "Einfrieren" der Anwendung aus. Threads sind aktiv, stehen aber im Warten auf Ressourcen und warten endlos aufeinander.
Typische Hinweise:
- Die Anwendung reagiert plötzlich nicht mehr (manchmal nur unter bestimmter Last).
- Der Debugger zeigt, dass Threads in lock, Monitor.Enter, WaitOne, EnterReadLock oder ähnlichen Synchronisationsoperationen hängen.
- Systemwerkzeuge (z. B. Task Manager oder die Tools in Rider/Visual Studio) zeigen 0% CPU-Auslastung — "alles steht".
- In den Logs — keine Fehler, aber auch keine Aktivität.
Wenn ihr JetBrains Rider oder Visual Studio verwendet, schaut mit dem Debugger nach, wo eure Threads hängen (Stack Trace). Wenn ihr seht, dass mehrere Threads in lock/Mutex.WaitOne aufeinander warten — herzlichen Glückwunsch (oder eher: mein Beileid): das ist ein Deadlock!
Klassische Szenarien für die Entstehung von Deadlocks
Uneinheitliche Reihenfolge der Locks. Wie in unserem Beispiel oben: Thread 1 sperrt lockerA, dann lockerB; Thread 2 macht es umgekehrt. Fertig ist die Falle.
Verschachtelte Locks. Wenn innerhalb eines lock-Blocks ihr einen weiteren lock auf einem anderen Objekt macht.
Überlappende Ressourcen in Methoden. Besonders schwer zu verfolgen, wenn Locks über verschiedene Methoden/Klassen verteilt sind und in unterschiedlichen Kombinationen vorkommen. Szenarien werden komplizierter, wenn es mehr als zwei Locks gibt.
3. Deadlock vermeiden
Gute Nachricht: gegen Deadlocks kann man sich schützen!
Schlechte Nachricht: man muss beim Design von Multithreading-Code vorsichtiger sein.
Hier ein paar pragmatische Empfehlungen (merkt sie euch — das gibt Pluspunkte im Interview!):
Locks immer in derselben Reihenfolge nehmen
Wenn ihr mehrere Objekte habt, die zusammen gesperrt werden können — definiert eine Gesamtreihenfolge (z. B. nach Name oder Index) und haltet euch immer daran.
Beispiel — Locks richtig nehmen
static void SafeLock(object objA, object objB)
{
// Vergleichen der Referenzen — so sperren Threads Objekte immer in derselben Reihenfolge
object first = objA.GetHashCode() < objB.GetHashCode() ? objA : objB;
object second = objA.GetHashCode() < objB.GetHashCode() ? objB : objA;
lock (first)
{
lock (second)
{
// kritische Sektion
}
}
}
Hier treten beide Threads zuerst in lock(objA) und dann in lock(objB) ein, falls objA "kleiner" ist als objB.
Benutzt Timeouts
Wenn ein Thread einen Lock nicht innerhalb einer sinnvollen Zeit bekommen kann — lasst ihn eine Exception werfen oder sauber abbrechen. Monitor.TryEnter eignet sich dafür.
Beispiel mit Monitor.TryEnter
bool lockTakenA = false;
bool lockTakenB = false;
try
{
Monitor.TryEnter(lockerA, 500, ref lockTakenA);
if (!lockTakenA)
{
// Konnte lockerA nicht in 500 ms bekommen — aufgeben, um keinen Deadlock zu riskieren
return;
}
Monitor.TryEnter(lockerB, 500, ref lockTakenB);
if (!lockTakenB)
{
return;
}
// kritische Sektion...
}
finally
{
if (lockTakenB)
Monitor.Exit(lockerB);
if (lockTakenA)
Monitor.Exit(lockerA);
}
Wenn ein Lock nicht bekommen wurde — bleiben wir nicht hängen!
Minimiert die Dauer von Locks
Haltet in kritischen Sektionen nur den absolut notwendigen Code.
Mischt nicht verschiedene Synchronisationsmechanismen
Wenn ihr gleichzeitig lock, Mutex, Semaphore, ReaderWriterLockSlim benutzt — steigt das Risiko, durcheinander zu kommen und einen Deadlock zu provozieren.
4. Deadlock mit Mutex, Semaphore und ReaderWriterLockSlim
Wechselseitige Blockierungen können nicht nur mit klassischen lock oder Monitor auftreten, sondern auch mit anderen Synchronisationsmechanismen.
Deadlock mit Mutex
static Mutex mutexA = new Mutex();
static Mutex mutexB = new Mutex();
void Work1()
{
mutexA.WaitOne();
Thread.Sleep(100);
mutexB.WaitOne();
// kritische Sektion
mutexB.ReleaseMutex();
mutexA.ReleaseMutex();
}
Wenn ein zweiter Thread diese in umgekehrter Reihenfolge nimmt — bekommt ihr einen Deadlock.
Deadlock mit ReaderWriterLockSlim
ReaderWriterLockSlim ist zwar flexibel, schützt aber nicht vor wechselseitigen Blockierungen, wenn ein Thread bereits einen Lock eines Typs hat und versucht, einen anderen zu bekommen.
Beispielsweise: wenn ein Thread einen read-lock (EnterReadLock) hält und versucht, in einen write-lock (EnterWriteLock) zu wechseln — ist ein Deadlock wahrscheinlich, weil ein write-lock nicht genommen werden kann, solange noch alle read-locks nicht freigegeben sind.
5. Wie Deadlocks in realen Anwendungen vermeiden
Schauen wir, wie das in einer Demo-Anwendung aussehen könnte. Angenommen, wir entwickeln einen Simulator für einen Online-Shop, in dem gleichzeitig laufen:
- Threads, die Lagerbestände aktualisieren (Writer)
- Threads, die Verfügbarkeit prüfen (Reader)
- Ein Admin-Thread, der sowohl liest als auch schreibt
Schlecht:
lock (stockLock)
{
// Lager aktualisieren
lock (userLock)
{
// etwas mit Benutzern
}
}
// ... und irgendwo anders genau umgekehrt!
lock (userLock)
{
lock (stockLock) { }
}
Gut:
Einigt euch — versucht immer zuerst stockLock zu sperren und danach userLock in allen Threads.
6. Wie man keine Deadlocks bei der Arbeit und im Interview bekommt
In echten Projekten:
- Beim Design — zeichnet die Struktur der Threads und Locks (auf Papier/Tafel).
- Teilt die Vereinbarung über die Reihenfolge der Locks mit dem ganzen Team.
- Benutzt statische Analyse-Tools (Rider/Visual Studio, ReSharper) — sie können potenzielle Deadlocks finden.
- Lest die Artikel in den Microsoft Docs zu Deadlock.
Im Interview:
- Zeigt, dass ihr das Problem und seine klassischen Symptome versteht.
- Erwähnt die Hauptschutzmechanismen: einheitliche Reihenfolge, Timeouts (TryEnter), minimale kritische Sektionen.
- Gebt ein Beispiel mit TryEnter und erklärt, warum es wichtig ist, alle genommenen Ressourcen im finally freizugeben.
7. Typische Fehler und unerwartete Deadlocks
Fehler Nr.1: nicht offensichtliche Locks in Drittanbieter-Bibliotheken.
Klassiker — Logging innerhalb einer kritischen Sektion. Der Thread hängt, und ihr ahnt nicht, dass die Ursache in fremdem Code liegt.
Fehler Nr.2: wiederholtes Sperren derselben Ressource.
Das ist ein reentrant deadlock: ein Thread versucht, in eine bereits gehaltene Sperre einzutreten, aber der Mechanismus erlaubt keinen reentranten Eintritt.
Fehler Nr.3: Nutzung von asynchronen Methoden innerhalb kritischer Sektionen.
Besonders gefährlich mit await: der Thread kann die Kontrolle im denkbar ungünstigsten Moment freigeben und das System blockieren.
GO TO FULL VERSION