Synchronisation: Nebenläufige Prozesse sicher steuern
Du erkennst Fehler durch verschränkte Abläufe, schützt gemeinsame Daten und unterscheidest Warteverfahren sowie Störungen ohne Fortschritt.
Kurz gesagt
Synchronisation stimmt nebenläufige Abläufe beim Zugriff auf gemeinsame Daten ab. Ein passender Schutz ordnet konkurrierende Zugriffe; dafür müssen die beteiligten Zugriffe dieselben Schutzregeln einhalten.
Ein Prozess ist ein laufendes Programm; ein Thread ist ein Ausführungsstrang darin. Threads können nebenläufig arbeiten: Ihre Ausführung überlappt sich zeitlich, auch wenn einzelne Schritte nacheinander stattfinden. Du untersuchst ausgedachte, vereinfachte Abläufe und passende Schutzregeln.
Deine Lernziele
Eine Race Condition liegt hier vor, wenn die zeitliche Reihenfolge konkurrierender Zugriffe darüber entscheidet, ob das beabsichtigte Ergebnis entsteht. Konkurrierend bedeutet hier: Mehrere Threads greifen auf dieselben Daten zu, und mindestens einer verändert sie.
Im betrachteten Modell besteht eine Änderung aus drei Schritten: Lesen, Rechnen und Zurückschreiben. Jeder Thread rechnet mit seiner eigenen gelesenen Kopie. Beim Zurückschreiben ersetzt sein Ergebnis den gerade gespeicherten Wert. Dieses Modell zeigt eine mögliche Zerlegung; es legt keine genaue Schrittfolge für jede echte Programmanweisung fest.
Verschränkt heißt: Schritte eines anderen Ablaufs liegen zwischen den Schritten einer noch nicht abgeschlossenen Änderung. Lesen zwei Threads denselben Ausgangswert, kann der spätere Schreibzugriff eine bereits gespeicherte Änderung überschreiben. Man spricht dann von einem verlorenen Update, also einer verlorenen Änderung.
Nicht jede Verschränkung führt zu einem Fehler. Schließt ein Thread seine Änderung ab, bevor der andere liest, kann der zweite mit dem neuen Wert weiterrechnen. Ein richtiger Endwert in einem Durchlauf beweist deshalb nicht, dass andere mögliche Abläufe ebenfalls richtig enden.
Race Condition
Ein Fehler durch die zeitliche Reihenfolge konkurrierender Zugriffe: Je nach Verschränkung entsteht das beabsichtigte Ergebnis oder eine Änderung geht verloren.
Beispiel: Ein gemeinsamer Punktestand – ausgedachtes Ablaufmodell
- 1Gespeichert sind 20 Punkte. Thread A soll 4 Punkte ergänzen, Thread B soll 7 ergänzen. Beide Änderungen sollen zählen.
- 2A liest 20 in seine eigene Kopie.
- 3B liest ebenfalls 20 in seine eigene Kopie.
- 4A berechnet 24 und schreibt 24 zurück.
- 5B berechnet mit seiner gelesenen 20 den Wert 27 und schreibt 27 zurück.
- 6Am Ende sind 27 Punkte gespeichert. Mit beiden Änderungen wären es 31. Die Änderung von A wurde überschrieben.
Teste dich
Ein kritischer Abschnitt greift auf gemeinsame veränderliche Daten zu und braucht Schutz, wenn sich konkurrierende Zugriffe stören können. Gehören mehrere Änderungen zusammen, muss der Schutz die gesamte Änderung umfassen. Dazu gehören auch die Lesezugriffe, deren Ergebnisse für die Änderung gebraucht werden.
Mutex steht für englisch „mutual exclusion“, auf Deutsch „gegenseitiger Ausschluss“. Ein Mutex ist eine Sperre: Ein Thread erwirbt sie vor dem kritischen Abschnitt und gibt sie danach frei. Nur ein Thread kann denselben Mutex gleichzeitig besitzen. Andere Threads, die ihn erwerben wollen, müssen warten. Verschiedene Mutexe schließen ihre Besitzer dagegen nicht gegenseitig aus.
Als konkretes Beispiel betrachten wir die eingebaute Objektsperre der Programmiersprache Java. Sie heißt auch Monitor und verwirklicht das Mutex-Prinzip. Beim Versuch, eine bereits belegte solche Sperre zu erwerben, blockiert ein anderer Thread: Er kann an dieser Stelle erst weiterarbeiten, wenn er sie erhält. Gibt ein Thread die Sperre frei und erwirbt danach ein anderer dieselbe Sperre, werden dem zweiten die vorherigen Änderungen des ersten sichtbar.
Zusammengehörige Änderungen sind damit gegenüber Zugriffen geschützt, die denselben Mutex passend verwenden. Ein Thread, der ein zusammengehöriges Wertepaar liest, muss ihn ebenfalls während des gesamten Lesens halten. Ein Zugriff ohne diese Sperre erhält durch sie weder gegenseitigen Ausschluss noch die genannte Sichtbarkeitsgarantie. Eine Sperre um nur einen Teil der zusammengehörigen Änderung reicht ebenfalls nicht.
Beispiel: Zwei virtuelle Vorräte umbuchen – ausgedachtes Modell
- 1Ein Programm verwaltet zwei Vorräte: links 9 Stück, rechts 4 Stück. Drei Stück sollen von links nach rechts wechseln.
- 2Der ändernde Thread erwirbt Mutex M, bevor er die Vorräte liest und verändert.
- 3Er setzt den linken Vorrat auf 6. Vorübergehend stehen damit 6 und 4 im Speicher.
- 4Er setzt den rechten Vorrat auf 7 und gibt erst danach M frei. Insgesamt sind wieder 13 Stück erfasst.
- 5Ein anderer Thread, der beide Vorräte unter demselben M liest, kann den Zwischenstand nicht während dieser Änderung lesen. Ein ungeschützter Zugriff erhält diese Garantie nicht.
Merke: Derselbe Mutex muss den gesamten zusammengehörigen Zugriff schützen. Dazu gehören passende Lesezugriffe und alle Teile der Änderung.
Teste dich
Beim bisher betrachteten Java-Monitor blockiert der Erwerb, wenn ein anderer Thread die Sperre besitzt. Ein Spinlock ist dagegen eine Sperre mit aktivem Warten: Der Thread prüft die belegte Sperre wiederholt selbst und verbraucht dabei Rechenzeit. Diese Unterscheidung beschreibt das betrachtete Warteverhalten. Sie behauptet keine einheitliche interne Umsetzung aller Mutexe.
Ein Semaphor verwaltet eine Anzahl von Erlaubnissen. Wir betrachten das Zählmodell des Java-Semaphors und Anfragen nach jeweils einer Erlaubnis. Ein erfolgreicher Erwerb nimmt eine Erlaubnis weg, eine Freigabe fügt eine hinzu. Ist keine verfügbar, blockiert der anfordernde Thread bis zum Erwerb. Die Erlaubnisse werden gezählt; sie sind keine eigenen Gegenstände. Fordert jeder Nutzer vor dem Zugang eine Erlaubnis an und gibt sie nach dem Verlassen genau einmal zurück, begrenzt die Anfangszahl die gleichzeitigen Nutzer.
Eine Ressource ist ein benötigtes Mittel, etwa ein verfügbarer Geräteplatz. Ein Semaphor mit mehreren Erlaubnissen kann den Zugang zu solchen Mitteln begrenzen. Er schützt gemeinsam veränderte Verwaltungsdaten aber nicht von selbst vor störenden Zugriffen. Dafür ist zusätzlicher passender Schutz nötig. Im betrachteten Semaphor-Modell gibt es keinen Besitzer wie beim Mutex; auch ein anderer Thread kann eine Erlaubnis freigeben.
Die folgenden Einsatzgrenzen für Spinlocks beziehen sich auf POSIX-Sperren im Benutzerbereich. POSIX steht für „Portable Operating System Interface“ und bezeichnet einen Standard für Schnittstellen zum Betriebssystem. Das Betriebssystem ist die Grundsoftware, die unter anderem Rechenzeit verteilt. Benutzerbereich meint normalen Programmcode außerhalb des zentralen Betriebssystemteils.
In diesem Kontext sollen die meisten Programme einen Mutex verwenden. Spinlocks sind vor allem für besonders abgestimmte Programme mit geeigneten Echtzeitregeln vorgesehen. Das sind besondere Regeln des Betriebssystems für die Zuteilung von Rechenzeit. Erhält der Besitzer einer Sperre vorübergehend keine Rechenzeit, können andere Threads ihre Zeit mit erfolglosem Prüfen verbrauchen. Eine feste Obergrenze der Wartezeit ist nicht zugesichert. Auch ein Thread mit höherem Vorrang kann auf einen Besitzer mit niedrigerem Vorrang warten. Kurze kritische Abschnitte allein sind deshalb kein ausreichender Grund für einen Spinlock.
| Verfahren | Warteverhalten im betrachteten Fall | Einsatz |
|---|---|---|
| Mutex am Java-Monitor | Der Erwerb blockiert bei belegter Sperre. | Einen kritischen Abschnitt gegenüber Zugriffen mit derselben Sperre schützen. |
| Java-Semaphor | Der Erwerb blockiert ohne verfügbare Erlaubnis. | Mit passender Nutzung eine festgelegte Zahl gleichzeitiger Nutzer zulassen. |
| POSIX-Spinlock im Benutzerbereich | Der Thread prüft wiederholt und verbraucht Rechenzeit. | Sperre für besonders abgestimmte Programme mit geeigneten Echtzeitregeln. |
Merke: Ein binärer Semaphor hat zwei Zustände: eine Erlaubnis verfügbar oder keine. Startet er mit einer Erlaubnis und stellt die Nutzung sicher, dass nie mehr als eine verfügbar wird, kann er gegenseitigen Ausschluss leisten. Die fehlende Besitzerbindung unterscheidet ihn weiterhin vom betrachteten Mutex.
Teste dich
Bei einem Deadlock warten mindestens zwei Threads dauerhaft aufeinander. Unter den gegebenen Regeln kann keiner den nächsten nötigen Schritt ausführen. Beispielsweise kann jeder eine Sperre behalten und auf eine zweite warten, die der andere besitzt. Ohne einen Eingriff von außen endet dieses gegenseitige Warten dann nicht.
Ein Deadlock kann mit blockierendem Warten auftreten. Bei Spinlocks ist aber auch dauerhaftes aktives Prüfen möglich: Die Threads verbrauchen Rechenzeit und warten trotzdem erfolglos aufeinander. Rechenaktivität allein schließt einen Deadlock deshalb nicht aus.
Bei einem Livelock reagieren Threads fortwährend aufeinander und führen dabei weitere Aktionen aus. Gerade diese gegenseitigen Reaktionen verhindern den angestrebten Fortschritt. Sie warten also nicht bloß dauerhaft auf die nächste nötige Handlung des jeweils anderen.
Verhungern bedeutet hier: Ein Thread bekommt über lange Zeit keinen ausreichenden Zugang zu einer benötigten Ressource und kommt deshalb nicht voran. Andere nutzen diese Ressource weiter. Eine kurze Wartezeit allein ist noch kein Verhungern. Gegenseitiger Ausschluss garantiert für sich genommen keinen regelmäßigen Zugang für jeden Thread.
Prüfe deshalb die Ursache des fehlenden Fortschritts: Besteht dauerhaftes gegenseitiges Warten, fortgesetztes gegenseitiges Reagieren oder ausbleibender Ressourcenzugang für einen Thread?
| Störung | Beobachtung | Entscheidender Grund |
|---|---|---|
| Deadlock | Dauerhaftes gegenseitiges Warten | Keiner erfüllt die Bedingung, auf die der andere wartet. |
| Livelock | Fortgesetztes gegenseitiges Reagieren | Die Reaktionen verhindern den angestrebten Fortschritt. |
| Verhungern | Ein Thread bekommt lange keinen ausreichenden Zugang. | Andere nutzen die benötigte Ressource weiter. |
Beispiel: Zugang zu einer gemeinsamen Datei – ausgedachtes Modell
- 1A und B benötigen dieselbe Sperre, um in eine gemeinsame Datei zu schreiben.
- 2In diesem Ablauf bekommt A die Sperre immer wieder. B erhält sie über lange Zeit nicht.
- 3A schreibt weiter, während B seinen nächsten Schreibschritt nicht ausführen kann.
- 4Das ist Verhungern von B. Eine kurze Wartezeit allein würde für diese Einordnung nicht reichen.
Teste dich
Alles auf einen Blick
Synchronisation
Beim Lesen, Rechnen und Zurückschreiben kann eine Änderung durch einen späteren Schreibzugriff verloren gehen.
Zusammengehörige Zugriffe müssen denselben Mutex für den gesamten geschützten Vorgang verwenden.
Mehrere Semaphor-Erlaubnisse begrenzen gleichzeitige Nutzer, ersetzen aber keinen Schutz gemeinsamer Verwaltungsdaten.
Aktives Prüfen verbraucht Rechenzeit; im betrachteten POSIX-Benutzerbereich ist sein Einsatz an besondere Bedingungen gebunden.
Deadlock bedeutet dauerhaftes gegenseitiges Warten, Livelock fortgesetztes gegenseitiges Reagieren und Verhungern ausbleibenden Ressourcenzugang.
Vier Geräteplätze vergeben – ausgedachtes Modell
- 1Annahme: Jeder Nutzer braucht genau einen Geräteplatz und fordert vor dem Zugang eine Erlaubnis an. Er gibt sie nach dem Verlassen genau einmal zurück. Der Semaphor startet mit vier Erlaubnissen.
- 2A und B erhalten je eine Erlaubnis. Es bleiben zwei; beide dürfen je einen Platz nutzen.
- 3C und D erhalten ebenfalls je eine Erlaubnis. Jetzt ist keine frei; vier Nutzer haben Zugang.
- 4E fordert eine Erlaubnis an und blockiert, solange keine verfügbar ist.
- 5B verlässt seinen Platz und gibt seine Erlaubnis zurück. Dadurch steht eine Erlaubnis zur Verfügung.
- 6Annahme: Kein anderer Thread nimmt diese Erlaubnis vorher. E erhält sie; wieder sind null Erlaubnisse frei. A, C, D und E haben Zugang.
- 7Ergebnis: Höchstens vier Nutzer haben zugleich Zugang. Gemeinsam veränderte Verwaltungsdaten brauchen zusätzlich passenden Schutz, etwa denselben Mutex für alle betroffenen Zugriffe.
Fehler finden
Fehler in Zeile 4Die Störung ist falsch benannt. Richtig: Das dauerhafte gegenseitige Warten ist hier ein Deadlock. Das aktive Prüfen ändert nichts daran, dass jeder auf die vom anderen festgehaltene Sperre wartet.
Lückentext
Zwei Threads müssen jeweils eine Nachricht des anderen erhalten, bevor sie selbst senden können. Beide warten dauerhaft und niemand greift ein. Das ist ein . Ein Programm soll drei gleichzeitige Nutzer zulassen und einen vierten bis zur Freigabe eines Platzes warten lassen. Dafür eignet sich ein passend eingerichteter . Ein Thread prüft eine belegte Sperre wiederholt selbst und verbraucht dabei Rechenzeit. Das ist .