Kritischer Abschnitt: Race Conditions verstehen
Ein kritischer Abschnitt ist ein Codebereich, den bezüglich derselben gemeinsam genutzten Ressource höchstens ein Thread oder Prozess gleichzeitig ausführen darf. So bleiben mehrteilige Änderungen konsistent und ihre Zwischenzustände für konkurrierende Zugriffe verborgen.
Hake ab, was du schon kannst — und komm am Ende hierher zurück!
Woran erkennst du einen kritischen Abschnitt?
Stell dir vor, mehrere Threads verändern denselben Zähler. Jeder Thread arbeitet für sich korrekt. Trotzdem kann ein falsches Gesamtergebnis entstehen, wenn ihre Anweisungen ungünstig ineinandergreifen.
Kritischer Abschnitt
Ein kritischer Abschnitt ist eine Folge von Anweisungen, die auf eine gemeinsam genutzte Ressource oder einen gemeinsamen Zustand zugreift und deshalb gegenüber korrespondierenden Zugriffen geschützt werden muss. Beispiele für solche Ressourcen sind Variablen, Datenstrukturen, Speicherbereiche, Verbindungen oder Geräte.
Entscheidend sind drei Fragen:
- Greifen mehrere Threads oder Prozesse auf dieselbe Ressource zu?
- Verändert mindestens einer von ihnen den gemeinsamen Zustand?
- Kann eine Operation in mehrere sichtbare Einzelschritte zerfallen?
Wenn alle drei Punkte zutreffen, kann ein kritischer Abschnitt vorliegen.
„Kritisch“ bedeutet nicht, dass im gesamten Programm nur ein Thread laufen darf. Ausgeschlossen werden nur konkurrierende kritische Abschnitte, die dieselbe Ressource betreffen.
Ein kritischer Abschnitt muss auch nicht ohne jede Unterbrechung ablaufen. Der Scheduler darf einen Thread pausieren. Währenddessen darf jedoch kein anderer Thread einen korrespondierenden kritischen Abschnitt für dieselbe Ressource betreten.
Wie entsteht eine Race Condition?
Eine Race Condition oder Wettlaufbedingung liegt vor, wenn das Ergebnis von der zeitlichen Reihenfolge nebenläufiger Ereignisse abhängt.
Der gemeinsame Zähler beginnt bei 0. Zwei Threads X und Y sollen ihn jeweils einmal erhöhen. Eine Erhöhung besteht aus drei Schritten:
- gemeinsamen Wert lokal einlesen;
- lokalen Wert um
1erhöhen; - lokalen Wert zurückschreiben.
Bei einer ungünstigen Verschränkung geschieht Folgendes:
| Schritt | Thread X | Thread Y | Gemeinsamer Zähler |
|---|---|---|---|
| 1 | liest 0 | – | 0 |
| 2 | – | liest 0 | 0 |
| 3 | erhöht lokal auf 1 | – | 0 |
| 4 | – | erhöht lokal auf 1 | 0 |
| 5 | schreibt 1 | – | 1 |
| 6 | – | schreibt 1 | 1 |
Beide Threads haben eine Erhöhung ausgeführt. Trotzdem lautet der Endwert 1 statt 2, weil Y das gleichlautende Ergebnis von X überschreibt. Das verlorene Ergebnis heißt verlorenes Inkrement.
Der kritische Abschnitt umfasst hier die gesamte Folge aus Lesen, Erhöhen und Zurückschreiben. Würde man nur den letzten Schreibzugriff schützen, könnten weiterhin beide Threads denselben alten Wert lesen.
Welche Abschnitte müssen gemeinsam geschützt werden?
Nicht die Schreibweise oder Position des Codes entscheidet über den gemeinsamen Schutz, sondern die betroffene Ressource.
Angenommen, ein Abschnitt erhöht zaehler, während ein anderer ihn verringert. Obwohl die Abschnitte verschiedene Aufgaben haben, verändern beide dieselbe Variable. Sie sind deshalb korrespondierende kritische Abschnitte und benötigen denselben wechselseitigen Ausschluss.
Wechselseitiger Ausschluss
Wechselseitiger Ausschluss bedeutet: Bezüglich einer bestimmten Ressource befindet sich zu einem Zeitpunkt höchstens ein Thread in einem zugehörigen kritischen Abschnitt.
Beispiel mit Java
In Java kann eine synchronisierte Instanzmethode so aussehen: public synchronized void increment() { count++; }
Bei Aufrufen derselben Counter-Instanz führt jeweils nur ein Thread diese synchronisierte Methode aus. Zwei verschiedene Instanzen besitzen dagegen getrennte Monitore und schützen sich nicht gegenseitig.
Zwei Threads verwenden dieselbe Counter-Instanz und rufen jeweils 1000-mal die synchronisierte Methode increment() auf. Wenn count bei 0 beginnt, keine weiteren Änderungen erfolgen und das Programm mit join() auf beide Threads wartet, sind alle 2000 Aufrufe abgeschlossen. Der Endwert ist dann 2000.
Die Sperre muss zur Ressource passen: Gleiche Ressource bedeutet gemeinsamer Schutz. Verschiedene Ressourcen können unabhängig geschützt werden.
Was zeichnet eine gute Schutzlösung aus?
Eine Sperre verhindert nicht automatisch jedes Problem. Eine Lösung für kritische Abschnitte soll vier Anforderungen erfüllen:
- Wechselseitiger Ausschluss: Höchstens ein Thread befindet sich bezüglich einer Ressource im kritischen Abschnitt.
- Fortschritt: Ein beendeter oder außerhalb seines kritischen Abschnitts stehender Thread darf die übrigen Threads nicht am Fortschritt hindern.
- Begrenzte Wartezeit: Kein wartender Thread darf beliebig lange ausgeschlossen bleiben.
- Geschwindigkeitsunabhängigkeit: Die Lösung darf keine bestimmte relative Ausführungsgeschwindigkeit der Threads voraussetzen.
Als Synchronisationsmittel kommen unter anderem synchronized, Locks, Monitore und Semaphore infrage. Sie ordnen den Zutritt zum kritischen Abschnitt. Welche Lösung passt, hängt vom betroffenen gemeinsamen Zustand und vom benötigten Ausschluss ab.
Warum sollte der geschützte Bereich klein bleiben?
Während ein Thread die passende Sperre hält, müssen andere interessierte Threads warten. Ein unnötig großer kritischer Abschnitt kann deshalb die Leistung verschlechtern. Falsch aufeinander abgestimmte Sperren können außerdem zu einem Deadlock führen, bei dem Threads dauerhaft aufeinander warten.
Klein bedeutet aber nicht unvollständig: Beim Zähler müssen Lesen, Ändern und Schreiben gemeinsam geschützt bleiben. Sonst besteht die Race Condition weiterhin.
Karteikasten
Überlege zuerst selbst und drehe die Karte anschließend zum Prüfen um.
Alles auf einen Blick
- Kritischer Abschnitt
- schützt gemeinsam genutzte Ressourcen
- umfasst zusammengehörige Mehrschrittoperationen
- verhindert Race Conditions durch wechselseitigen Ausschluss
- verbindet korrespondierende Codebereiche
- benötigt Fortschritt und begrenzte Wartezeit
- sollte vollständig und möglichst klein sein
Abschluss-Check
Wenn du einen kritischen Abschnitt beurteilst, gehe in dieser Reihenfolge vor: gemeinsame Ressource bestimmen, mehrteilige Zugriffe markieren, mögliche Verschränkung prüfen und anschließend einen gemeinsamen, vollständig abgegrenzten Schutz wählen.
Mit Google fortfahren