Monitor in der Informatik: Synchronisation verstehen
Hier geht es um den Monitor als Synchronisationskonzept, nicht um einen Bildschirm oder das Bildungsmonitoring. Ein Monitor bündelt gemeinsam genutzte Daten mit den Operationen darauf. Er sorgt dafür, dass kritische Zugriffe desselben Monitors nur von einem Thread zur Zeit ausgeführt werden. Bedingungsvariablen koordinieren zusätzlich das Warten auf einen passenden Zustand.
Hake ab, was du schon kannst — und komm am Ende hierher zurück!
Warum ein Monitor nötig ist
Stell dir zwei Threads vor, die auf dieselbe Datenstruktur zugreifen. Werden ihre Einzelschritte ungünstig verschachtelt, kann ein Thread mit einem veralteten Zwischenstand weiterarbeiten. Das Ergebnis kann dadurch widersprüchlich werden. Eine solche vom zeitlichen Ablauf abhängige Fehlersituation heißt Race Condition.
Monitor
Ein Monitor ist eine geschlossene Einheit aus gemeinsam genutzten Daten und den Prozeduren oder Methoden, die auf diese Daten zugreifen. Er steuert den Zutritt zu kritischen Operationen und kann Threads zusätzlich auf Bedingungen warten lassen.
Der Monitor hebt die Synchronisation auf ein höheres Abstraktionsniveau: Der Programmcode nutzt die vorgesehenen Monitor-Operationen. Übersetzung oder Laufzeitsystem realisieren die nötigen Synchronisationsprimitive. Damit liegt der Schutz direkt bei den Daten und ihren zulässigen Zugriffen.
Ein Monitor verwaltet einen gemeinsamen Puffer. ablegen() verändert den Puffer, entnehmen() ebenfalls. Beide Operationen gehören zum selben Monitor. Führt Thread A gerade ablegen() aus, muss Thread B vor entnehmen() warten, bis A den Monitor verlassen hat. So verändern beide den Puffer nicht gleichzeitig.
Wechselseitiger Ausschluss schützt kritische Abschnitte
Ein kritischer Abschnitt ist ein Teil einer Operation, der gemeinsam genutzte Daten so liest oder verändert, dass eine unkontrollierte Verschachtelung zu Fehlern führen könnte. Monitor-Operationen mit solchen Abschnitten werden unter wechselseitigem Ausschluss ausgeführt.
Bei demselben Monitor befindet sich höchstens ein Thread zur Zeit in einer geschützten Monitor-Operation. Ein weiterer Thread wird blockiert, bis der Monitor frei ist.
Das ähnelt einem Raum mit nur einem Platz: Befindet sich Thread A im Raum, wartet Thread B davor. Verlässt A den Raum, darf ein wartender Thread eintreten. Entscheidend ist, dass sich diese Regel auf denselben Monitor bezieht. Zwei verschiedene Monitorobjekte besitzen getrennte Zutrittskontrollen.
Nicht jede Prozedur eines Moduls muss einen kritischen Abschnitt enthalten. Der Monitor kann auch Zugriffsprozeduren anbieten, die keinen exklusiven Schutz benötigen. Geschützt werden muss genau der Teil, dessen unkontrollierte Ausführung den gemeinsamen Zustand gefährdet.
Thread A ruft eintragen() auf und betritt den Monitor. Während A den gemeinsamen Zustand ändert, ruft Thread B loeschen() am selben Monitor auf. B wird blockiert. Erst nachdem A die Operation beendet und den Monitor freigegeben hat, kann B eintreten. Die Änderungen laufen dadurch nacheinander ab.
Mit Bedingungen sinnvoll warten
Exklusiver Zutritt löst noch nicht jedes Problem. Ein Thread kann den Monitor betreten und feststellen, dass der gemeinsame Zustand eine Fortsetzung nicht erlaubt. Beim Puffer wäre entnehmen() zum Beispiel erst sinnvoll, wenn ein Element vorhanden ist.
Ständiges Verlassen, erneutes Eintreten und Prüfen wäre unkontrolliertes Warten. Monitore verwenden deshalb Bedingungsvariablen. Jede Bedingungsvariable steht für einen Wartegrund und besitzt eine eigene Warteschlange.
Bedingungsvariable
Eine Bedingungsvariable koordiniert Threads, die auf einen bestimmten Zustand der Monitordaten warten. Ob der benötigte Zustand tatsächlich vorliegt, stellt der Thread durch Prüfen der Daten fest.
Die beiden grundlegenden Operationen sind:
wait(): Der aktive Thread wird blockiert, gibt den Monitor frei und wird in die Warteschlange dieser Bedingungsvariablen eingereiht. Nun kann ein anderer Thread den Monitor betreten und den Zustand verändern.signal(): Ein Thread signalisiert die Bedingungsvariable. Ein dort wartender Thread wird deblockiert. Wartet niemand, hat das Signal keine Wirkung.
Ein Verbraucher betritt den Puffermonitor und erkennt: Der Puffer ist leer. Er ruft wait() auf der Bedingung nichtLeer auf. Dadurch gibt er den Monitor frei. Ein Erzeuger kann nun eintreten und ein Element ablegen. Danach ruft er signal() für nichtLeer auf. Der wartende Verbraucher wird zur Fortsetzung zugelassen. Wann genau er weiterläuft, hängt von der Monitorsemantik ab.
Hoare- und Mesa-Semantik unterscheiden
Nach signal() gibt es einen Konflikt: Der signalisierende Thread ist noch im Monitor, zugleich könnte der deblockierte Thread fortfahren. Da nur einer aktiv sein darf, legt die Monitorsemantik die Reihenfolge fest.
| Semantik | Reaktion auf signal() | Bedeutung für den Wartenden |
|---|---|---|
| Hoare-Typ, „Signal and Wait“ | Der signalisierende Thread wird blockiert. | Ein wartender Thread übernimmt den Monitor sofort und setzt bei wait() fort. |
| Mesa-Typ, „Signal and Continue“ | Der signalisierende Thread arbeitet im Monitor weiter. | Ein Wartender wechselt zunächst in die allgemeine Monitor-Warteschlange und tritt später ein. |
Beim Hoare-Typ gilt der signalisierte Zustand beim unmittelbaren Fortsetzen des Wartenden. Beim Mesa-Typ kann sich der Zustand bis zu dessen späterem Eintritt erneut ändern. Deshalb muss der Wartende die benötigte Bedingung dann erneut prüfen.
Der Puffer enthält nach dem Ablegen genau ein Element. Der Erzeuger signalisiert nichtLeer.
- Hoare-Typ: Ein wartender Verbraucher übernimmt den Monitor sofort. Der Erzeuger wartet.
- Mesa-Typ: Der Erzeuger darf zunächst weiterarbeiten und den Monitor später verlassen. Der Verbraucher konkurriert anschließend um den Zutritt und prüft erneut, ob der Puffer noch nicht leer ist.
Das Monitorprinzip in Java anwenden
In Java besitzt grundsätzlich jedes Objekt Monitorfähigkeiten. Eine mit synchronized gekennzeichnete Instanzmethode nutzt den Monitor des betreffenden Objekts. Rufen mehrere Threads synchronized-Methoden desselben Objekts auf, kann sich höchstens einer von ihnen zur Zeit in einer solchen Methode dieses Objekts befinden.
Ein Objekt puffer bietet die synchronisierten Methoden ablegen() und entnehmen() an. Thread A führt gerade puffer.ablegen() aus. Ruft Thread B gleichzeitig puffer.entnehmen() auf, wartet B am Monitor von puffer. Ein Aufruf an einem anderen Pufferobjekt hätte dagegen einen anderen Objektmonitor.
Java stellt dafür Methoden von Object bereit:
wait()blockiert den aufrufenden Thread und gibt den Monitor des Objekts frei.notify()deblockiert einen beliebigen Thread, der an diesem Monitor wartet. Dieser läuft erst weiter, wenn der Monitor frei ist; die Auswahl muss nicht fair sein.notifyAll()deblockiert alle dort wartenden Threads. Danach konkurrieren sie um den freien Monitor.
Ein deblockierter Thread prüft seine erwartete Bedingung erneut. Sie kann noch nicht gelten oder ein schnellerer Thread kann den passenden Zustand bereits wieder verändert haben.
notify() bedeutet nicht „Der Wartende läuft sofort“. Es bedeutet: Ein Wartender darf wieder um den Objektmonitor konkurrieren.
Karteikasten
Überlege zuerst selbst und drehe die Karte anschließend zum Prüfen um.
Alles auf einen Blick
- Monitor
- schützt gemeinsame Daten
- kritische Abschnitte
- wechselseitiger Ausschluss
- koordiniert Bedingungen
- `wait()` gibt den Monitor frei
- `signal()` deblockiert einen Wartenden
- bestimmt die Fortsetzung
- Hoare: Wartender sofort
- Mesa: Signalisierender zuerst
- erscheint in Java
- `synchronized` schützt Objektzugriffe
- `notify()` und `notifyAll()` wecken Wartende
- schützt gemeinsame Daten
Mit Google fortfahren