Mutex einfach erklärt: Gemeinsame Daten schützen
Du lernst, gemeinsame Daten mit einem Mutex zu schützen, Verklemmungen zu erkennen und die Aussagekraft von Tests zu beurteilen.
Kurz gesagt
Ein Mutex ist hier eine Sperre mit einem Besitzer. Er schützt gemeinsame Daten, wenn alle beteiligten Zugriffe dieselbe passende Sperre für den ganzen kritischen Abschnitt beachten.
Ein Thread ist ein Ausführungsstrang innerhalb eines Programms. Nebenläufige Threads können ihre Arbeitsschritte abwechselnd oder auf mehreren Prozessorkernen gleichzeitig ausführen. Greifen sie auf gemeinsame Daten zu, müssen ihre Schritte passend aufeinander abgestimmt sein. Die folgenden ausgedachten Modelle zeigen solche Abläufe vereinfacht und setzen zuverlässig umgesetzte Sperren voraus.
Deine Lernziele
Eine Änderung kann aus mehreren Schritten bestehen: einen gemeinsamen Wert lesen, daraus einen neuen Wert bilden und diesen zurückschreiben. Ohne passenden Schutz können sich Schritte verschiedener Threads dazwischenschieben. Beide können dann mit demselben alten Stand arbeiten. Schreibt einer später sein Ergebnis zurück, kann er die Änderung des anderen überschreiben. Die Daten sind dann inkonsistent: Sie passen nicht mehr zu den Änderungen, die das Programm eigentlich ausführen sollte. Dieser Fehler ist eine Race Condition, also ein Fehler, dessen Auftreten von der zeitlichen Reihenfolge unzureichend abgestimmter Zugriffe abhängt.
Der kritische Abschnitt umfasst hier das zusammengehörige Lesen, Ändern und Zurückschreiben. Auch eine Prüfung mit anschließender Änderung muss gemeinsam geschützt werden, wenn ein anderer Thread den geprüften Zustand inzwischen verändern könnte. Ein Mutex ermöglicht gegenseitigen Ausschluss: Solange ein Thread die Sperre besitzt, kann kein anderer dieselbe Sperre erwerben. Die Entscheidung über den Erwerb muss unteilbar erfolgen, damit nicht zwei Threads gleichzeitig Besitzer werden.
Ein Thread erwirbt die Sperre vor dem kritischen Abschnitt und gibt sie danach frei. Alle beteiligten Zugriffe müssen dieselbe passende Sperre beachten. Das gilt auch für Leser, die mehrere zusammengehörige Angaben als stimmigen Stand benötigen. Verschiedene Sperren für dieselben Daten verhindern die störende Überlagerung nicht. Nur das Zurückschreiben zu schützen reicht ebenfalls nicht: Zwei Threads könnten vorher denselben alten Stand gelesen haben. Die Freigabe muss auch bei einem vorzeitigen Ende durch einen Fehler sichergestellt sein. Diese Abläufe beschreiben ein Modell, keinen ausführbaren Programmcode.
Mutex
Eine Sperre für gegenseitigen Ausschluss. Im hier verwendeten besitzgebundenen Modell erwirbt ein Thread die Sperre, besitzt sie während seines geschützten Zugriffs und gibt sie anschließend selbst frei.
Beispiel: Zwei Ergänzungen eines Protokolls – ausgedachtes Modell
- 1Der gemeinsame Text lautet zunächst „Los“. Ein Thread soll „ A“, der andere „ B“ anhängen.
- 2Beide lesen zunächst „Los“ und bilden daraus getrennt „Los A“ und „Los B“.
- 3Der erste schreibt „Los A“ zurück. Danach schreibt der zweite „Los B“ zurück.
- 4Am Ende fehlt „ A“. Bei vollständig nacheinander ausgeführten Ergänzungen wären beide Zusätze erhalten geblieben.
- 5Schützen beide das gesamte Lesen, Ergänzen und Zurückschreiben mit demselben Mutex, kann diese Überlagerung nicht auftreten.
Merke: Eine Sperre schützt Daten durch die gemeinsame Zugriffsregel. Der ganze zusammengehörige Zugriff gehört unter dieselbe passende Sperre.
Teste dich
Im hier behandelten Mutex-Modell gehört eine erworbene Sperre einem bestimmten Thread. Dieser Besitzer gibt sie nach seinem geschützten Zugriff wieder frei. Ein anderer Thread kann ihm die belegte Sperre weder abnehmen noch sie stellvertretend freigeben. Ein wartender Thread darf den geschützten Zugriff erst nach seinem eigenen erfolgreichen Erwerb beginnen.
Ein konkretes Beispiel sind die eingebauten Monitorsperren der Programmiersprache Java. Eine Monitorsperre ist eine in Java eingebaute Sperre, die zu einem Objekt gehört. Ein Objekt bündelt Daten und zugehörige Programmaktionen. Beim Aufruf einer mit „synchronized“ gekennzeichneten Objektmethode, also einer entsprechend geschützten Programmaktion des Objekts, erwirbt der Thread dessen Monitorsperre. Beim Verlassen dieser Methode wird sie auch dann freigegeben, wenn ein unbehandelter Fehler den Aufruf beendet. Java-Monitorsperren sind reentrant: Ihr Besitzer kann dieselbe Sperre erneut erwerben. Das erlaubt etwa verschachtelte geschützte Aufrufe desselben Threads. Diese Eigenschaft darfst du nicht jedem Mutex zuschreiben.
Ein Semaphor verwaltet dagegen Erlaubnisse. Beim Erwerb wird eine verfügbare Erlaubnis entnommen; ohne verfügbare Erlaubnis muss der Thread warten. Laut Java-Dokumentation lässt sich ein Java-Semaphor binär nutzen: Anfangs gibt es eine Erlaubnis, und die Verwendung muss sicherstellen, dass höchstens eine verfügbar ist. Die Zustände sind dann „eine Erlaubnis verfügbar“ und „keine Erlaubnis verfügbar“. Der Semaphor kennt keinen Besitzer. Auch ein anderer Thread darf eine Erlaubnis zurückgeben. Gibt er sie zurück, während keine verfügbar ist, steigt die Anzahl auf eins. Zusätzliche Freigaben müssen so begrenzt werden, dass die binäre Nutzung erhalten bleibt. Aussagen über Besitz und Reentranz hängen von der konkreten Sperrart ab; die Java-Dokumentation behauptet diese Unterschiede nicht pauschal für jede mögliche Sperrimplementierung.
| Merkmal | Besitzgebundenes Mutex-Modell | Binär genutzter Java-Semaphor |
|---|---|---|
| Besitz | Der erfolgreiche Erwerber besitzt die Sperre. | Es gibt keinen Besitzer. |
| Freigabe | Der Besitzer gibt seine Sperre frei. | Auch ein anderer Thread darf eine Erlaubnis zurückgeben. |
| Erneuter Erwerb | Bei Java-Monitorsperren durch den Besitzer möglich; nicht für jeden Mutex zugesichert. | Der Erwerb benötigt eine verfügbare Erlaubnis. |
Merke: Zwei Zustände allein machen einen Semaphor noch nicht zum besitzgebundenen Mutex. Bei der binären Nutzung eines Java-Semaphors muss außerdem die Grenze von höchstens einer verfügbaren Erlaubnis eingehalten werden.
Teste dich
Ein Mutex kann Zugriffe ausschließen und trotzdem Teil einer Verklemmung sein. Eine Verklemmung, auch Deadlock genannt, liegt vor, wenn Threads dauerhaft aufeinander warten. Ein typisches Muster verwendet zwei Sperren: Der erste Thread hält eine Sperre und wartet auf die zweite. Der zweite hält die zweite und wartet auf die erste. Beide behalten während des Wartens ihre bereits erworbene Sperre. Deshalb kann keiner weiterarbeiten und seine Sperre freigeben.
Für dieses Zwei-Sperren-Muster hilft eine gemeinsame Erwerbsreihenfolge. Fordern beide Threads zuerst dieselbe erste Sperre und danach die zweite an, kann der beschriebene Wartekreis nicht entstehen. Der Verlierer beim ersten Erwerb hält dann noch keine der beiden Sperren. Das setzt voraus, dass beide diese Reihenfolge einhalten. Es ist kein allgemeiner Beweis gegen jede denkbare Verklemmung im restlichen Programm. Eine Pause vor der zweiten Anforderung oder eine geplante spätere Freigabe der zweiten Sperre beseitigt den beschriebenen Kreis nicht, solange beide ihre erste Sperre behalten.
Warten kann unterschiedlich umgesetzt sein. Beim blockierenden Warten ist der wartende Thread angehalten und prüft die Sperre nicht fortlaufend selbst. Beim aktiven Warten prüft er sie dagegen wiederholt in einer Schleife. Eine Sperre, die so wartet, heißt Spinlock. Solche Prüfungen verbrauchen Rechenzeit. Bekommt der Besitzer gerade keine Rechenzeit, können andere Threads weiter prüfen, ohne dass er die Sperre inzwischen freigeben kann. Bei einem Wartekreis können sie sogar dauerhaft prüfen. Nicht jeder Mutex wartet auf diese Weise.
Die Linux-Dokumentation empfiehlt Spinlocks in Benutzerprogrammen zusammen mit geeigneter Echtzeitplanung, also besonderen Regeln für die Zuteilung von Rechenzeit. Sie warnt vor ihrer Verwendung als allgemeine Sperrlösung. Die Wartezeit kann ohne feste obere Grenze wachsen. Außerdem droht Prioritätsumkehr: Ein wichtiger Thread wartet auf einen weniger wichtigen Besitzer, dessen Weiterarbeit durch andere Threads verzögert werden kann. Ein sorgfältiger Einsatz muss deshalb auch die Systemkonfiguration, die Zuordnung zu Prozessorkernen und die Prioritäten der Threads berücksichtigen.
Beispiel: Zwei Sperren für eine Medienbearbeitung – ausgedachtes Modell
- 1Thread „Bild“ besitzt Sperre F. Thread „Ton“ besitzt Sperre G.
- 2„Bild“ fordert G an und behält dabei F.
- 3„Ton“ fordert F an und behält dabei G.
- 4Beide warten auf die vom anderen gehaltene Sperre. Ohne eine Änderung dieses Zustands kann keiner fortfahren.
Merke: Eine gemeinsame Erwerbsreihenfolge verhindert den beschriebenen Zwei-Sperren-Kreis. Aktives Warten löst einen solchen Kreis nicht und verbraucht währenddessen Rechenzeit.
Teste dich
Ein fehlerfreier Testlauf zeigt, dass in diesem Lauf kein Fehler gefunden wurde. Er beweist keine allgemeine Fehlerfreiheit. Bei nebenläufigem Code hängen mögliche Fehler unter anderem davon ab, wo zwischen Threads gewechselt wird. Ein Lauf kann einen günstigen Ablauf treffen, obwohl eine andere Verschachtelung fehlerhaft wäre. Auch viele Wiederholungen beweisen weder Fehlerfreiheit für weitere Eingaben noch, dass eine Sperre den ganzen nötigen Abschnitt schützt.
Eine Belastungsprobe lässt das Programm lange oder mit viel Arbeit laufen. Dadurch können Fehler sichtbar werden, doch die geprüften Abläufe bleiben unvorhersehbar und möglicherweise lückenhaft. Gezielte Tests steuern dagegen die Threadwechsel an ausgewählten Stellen. Sie können beispielsweise dafür sorgen, dass beide Threads einen alten gemeinsamen Zustand gelesen haben, bevor einer seine Änderung zurückschreibt. So lässt sich ein vermuteter Fehler gezielt auslösen. Ein festgehaltener Ablauf hilft außerdem, denselben Fehler erneut hervorzurufen und seine Ursache zu untersuchen.
Ein Testszenario legt eine untersuchte Situation fest, etwa Eingaben, Anfangszustand und beteiligte Aktionen. Die Forschungsarbeit „CHESS: A systematic testing tool for concurrent software“ beschreibt ein Werkzeug, das die Verschachtelungen eines gegebenen Szenarios systematisch untersucht und fehlerhafte Abläufe reproduzierbar macht. Selbst eine vollständige Untersuchung gilt für das festgelegte Szenario und das verwendete Modell. Sie ersetzt keinen allgemeinen Korrektheitsbeweis für andere Eingaben, ausgelassene Aktionen oder weitere Systembedingungen. Fehlt beispielsweise ein Fehlerabbruch im Modell, muss ein zusätzliches Szenario diesen Abbruch berücksichtigen. Mehr Wiederholungen oder mehr Last ergänzen die ausgelassene Aktion nicht.
Beispiel: Einen doppelten Auftragsstart gezielt prüfen – ausgedachtes Testmodell
- 1Ein Auftrag soll genau einmal starten. Sein gemeinsamer Zustand ist zunächst „offen“.
- 2Ohne Sperre liest jeder Thread den Zustand und merkt sich bei „offen“, dass er den Auftrag starten soll.
- 3Der Test lässt zuerst beide Threads „offen“ lesen, bevor einer den Zustand verändert.
- 4Danach setzt jeder anhand seiner gemerkten Entscheidung den Zustand auf „gestartet“ und startet den Auftrag.
- 5Der Auftrag startet zweimal. Der gesteuerte Ablauf macht den Fehler sichtbar und lässt sich für die Fehlersuche wiederholen.
Merke: Gezielte Threadwechsel untersuchen ausgewählte Verschachtelungen. Auch ein vollständig geprüftes Szenario bleibt durch seine Eingaben, Aktionen und Modellannahmen begrenzt.
Teste dich
Alles auf einen Blick
Mutex
Alle beteiligten Zugriffe müssen dieselbe passende Sperre für den zusammengehörigen Abschnitt beachten.
Im besitzgebundenen Mutex-Modell gibt der Besitzer die Sperre frei; ein binär genutzter Java-Semaphor kennt keinen Besitzer.
Gegenseitiges Warten mit festgehaltenen Sperren kann den Fortschritt dauerhaft verhindern.
Wiederholtes Prüfen verbraucht Rechenzeit; Spinlocks sind keine allgemeine Sperrlösung für Benutzerprogramme.
Gezielte Threadwechsel helfen beim Finden und Wiederholen von Fehlern, ersetzen aber keinen allgemeinen Korrektheitsbeweis.
Zwei zusammengehörige Einstellungen schützen – ausgedachtes Modell
- 1Eine Anzeige speichert Farbe und Form getrennt. Erlaubte Paare sind „orange, Dreieck“ und „grün, Kreis“. Anfangs gilt „orange, Dreieck“.
- 2Ein Schreiber soll auf „grün, Kreis“ umstellen. Ein Leser soll beide Angaben als zusammengehöriges Paar lesen.
- 3Ohne Schutz könnte der Leser nach dem Ändern der Farbe, aber vor dem Ändern der Form lesen. Er bekäme das unerlaubte Paar „grün, Dreieck“.
- 4Wir legen deshalb einen gemeinsamen Mutex fest. Der Schreiber erwirbt ihn vor der ersten Änderung, ändert beide Angaben und gibt ihn danach frei.
- 5Der Leser erwirbt denselben Mutex vor dem Lesen, liest beide Angaben und gibt ihn danach frei.
- 6Für den Ablauf „Schreiber vollständig vor Leser“ erhält der Leser „grün, Kreis“. Für den umgekehrten Ablauf erhält er „orange, Dreieck“. Das unerlaubte Zwischenpaar bleibt ihm bei dieser Zugriffsregel verborgen.
Fehler finden
Fehler in Zeile 3Die Sperre wird zu spät erworben. Beide Threads könnten vorher denselben alten Listenstand lesen und später eine Ergänzung überschreiben. Richtig: Der gemeinsame Mutex muss bereits vor dem Lesen erworben und bis nach dem Zurückschreiben gehalten werden.
Lückentext
Beim gemeinsamen Ändern nach dem Muster „Lesen, Ändern, Zurückschreiben“ muss derselbe Mutex schützen. Im besitzgebundenen Mutex-Modell hat Thread A die Sperre erworben und noch nicht freigegeben. Eine stellvertretende Freigabe durch Thread B ist . Ein Java-Semaphor wird binär genutzt. Thread P entnimmt die einzige verfügbare Erlaubnis. Thread Q gibt anschließend eine Erlaubnis zurück. Diese Freigabe ist .