Erzeuger-Verbraucher-Problem einfach erklärt
Beim Erzeuger-Verbraucher-Problem legen Erzeuger Daten in einem gemeinsamen Puffer ab, während Verbraucher sie entnehmen. Damit keine Daten verloren gehen, müssen alle Beteiligten den Zugriff schützen und auf die Zustände leer und voll reagieren.
Du lernst, warum eine einfache Sperre nicht genügt, wie Semaphore die freien und belegten Plätze zählen und wie ein Java-Monitor Threads ohne ständiges Nachfragen warten lässt.
Hake ab, was du schon kannst — und komm am Ende hierher zurück!
Worum geht es bei dem Problem?
Stell dir eine Druckwarteschlange vor: Programme erzeugen Druckaufträge und legen sie in einer gemeinsamen Warteschlange ab. Ein Druckdienst entnimmt die Aufträge und verarbeitet sie. Die Warteschlange bildet den Puffer zwischen beiden Seiten.
Erzeuger-Verbraucher-Problem
Das Erzeuger-Verbraucher-Problem beschreibt die koordinierte Nutzung eines gemeinsamen Puffers durch mindestens einen Erzeuger und mindestens einen Verbraucher. Erzeuger legen Elemente ab; Verbraucher entnehmen und verarbeiten sie.
Der Puffer entkoppelt die Geschwindigkeiten: Ein Erzeuger darf zeitweise schneller arbeiten als ein Verbraucher, solange noch Platz vorhanden ist.
Ein unbeschränkter Puffer wird im Modell als unbegrenzt groß betrachtet. Ein beschränkter Puffer besitzt dagegen eine feste Kapazität:
- Ist er leer, muss ein Verbraucher auf ein Element warten.
- Ist er voll, muss ein Erzeuger auf einen freien Platz warten.
- Bei jeder Änderung muss der Zugriff auf die gemeinsame Datenstruktur geschützt sein.
Welche drei Bedingungen müssen erfüllt sein?
Eine korrekte Lösung verbindet Sicherheit und Lebendigkeit. Sicherheit bedeutet hier: Der Puffer gerät nicht in einen ungültigen Zustand. Lebendigkeit bedeutet: Ein wartender Thread kann weiterarbeiten, sobald seine Bedingung erfüllt ist.
1. Verändernde Zugriffe dürfen sich nicht überschneiden
Das Einfügen oder Entnehmen besteht meist aus mehreren Teilschritten. Würden zwei Threads diese Schritte unkontrolliert verschachteln, könnten Zähler, Elemente oder Zeiger nicht mehr zusammenpassen.
Kritischer Abschnitt
Ein kritischer Abschnitt ist ein Programmteil, der auf gemeinsam genutzte, veränderliche Daten zugreift und deshalb nicht gleichzeitig von konkurrierenden Threads ausgeführt werden darf.
Eine Sperre sorgt für gegenseitigen Ausschluss: Höchstens ein Thread verändert den Puffer zur selben Zeit.
2. Ein Verbraucher braucht ein vorhandenes Element
Ein leerer Puffer enthält nichts, das entnommen werden könnte. Der Verbraucher muss warten, ohne dabei die Sperre dauerhaft festzuhalten.
3. Ein Erzeuger braucht einen freien Platz
Bei einem beschränkten Puffer darf der Erzeuger nicht über dessen Kapazität hinaus schreiben. Auch er muss warten können, ohne die Gegenseite auszusperren.
Ein Puffer hat vier Plätze und ist voll. Ein Erzeuger möchte ein weiteres Element ablegen. Er darf nicht in den Puffer schreiben und dabei die einzige Puffersperre behalten. Sonst könnte kein Verbraucher die Sperre erhalten, ein Element entnehmen und dadurch Platz schaffen.
Eine Sperre schützt wie der Puffer verändert wird. Eine Zustandsbedingung entscheidet, ob Einfügen oder Entnehmen gerade erlaubt ist.
Wie koordinieren Semaphore den Puffer?
Ein Semaphor verwaltet eine Anzahl verfügbarer Ressourcen. Die Operation P(s) reserviert eine Ressource und verringert den Zähler. Ist keine Ressource verfügbar, wird der Thread blockiert. V(s) gibt eine Ressource frei, erhöht den Zähler und kann einen wartenden Thread wecken.
Für einen beschränkten Puffer der Kapazität N werden drei Semaphore verwendet:
| Semaphor | Anfangswert | Aufgabe |
|---|---|---|
mutex | 1 | schützt die Veränderung des Puffers |
sem_read | 0 | zählt entnehmbare Elemente |
sem_write | N | zählt freie Plätze |
Nach jeder vollständig abgeschlossenen Operation gilt:
$belegt + frei = N$
Für einen anfangs leeren Puffer mit N = 4 entwickeln sich die Zähler so:
| Zustand | sem_read | sem_write |
|---|---|---|
| Start: leer | 0 | 4 |
| nach einem Erzeuger | 1 | 3 |
| nach einem weiteren Erzeuger | 2 | 2 |
| nach einem Verbraucher | 1 | 3 |
Ablauf eines Erzeugers
- Das Element außerhalb des kritischen Abschnitts erzeugen.
- Mit
P(sem_write)einen freien Platz reservieren. - Mit
P(mutex)den exklusiven Zugriff erwerben. - Das Element ablegen und den Pufferzustand ändern.
- Mit
V(mutex)den Zugriff freigeben. - Mit
V(sem_read)ein entnehmbares Element melden.
Ablauf eines Verbrauchers
- Mit
P(sem_read)ein vorhandenes Element reservieren. - Mit
P(mutex)den exklusiven Zugriff erwerben. - Das Element entnehmen und den Pufferzustand ändern.
- Mit
V(mutex)den Zugriff freigeben. - Mit
V(sem_write)einen freien Platz melden. - Das Element außerhalb des kritischen Abschnitts verarbeiten.
Die Reihenfolge ist entscheidend: Zuerst wird ein freier Platz oder ein vorhandenes Element reserviert, danach der Mutex. So hält kein Thread den Mutex, während er auf einen Pufferzustand wartet, den nur die Gegenseite herstellen kann.
Bei einem unbeschränkten Puffer wird kein Semaphor für freie Plätze benötigt. Falls die verwendete Datenstruktur verändernde Zugriffe selbst sicher koordiniert, kann außerdem ein eigener mutex entfallen.
Wie funktioniert die Lösung mit Java-Monitoren?
Eine synchronisierte Java-Methode verbindet den geschützten Zugriff mit dem Monitor des Objekts. Zustände wie valueSet oder usedSlots zeigen an, ob die fachliche Bedingung erfüllt ist.
wait() bewirkt drei wichtige Dinge:
- Der aktuelle Thread wartet am Monitorobjekt.
- Er gibt dessen Sperre atomar frei.
- Nach einer Benachrichtigung muss er die Sperre erneut erwerben, bevor er fortfährt.
notifyAll() weckt alle Threads, die an diesem Monitor warten. Die Benachrichtigung bedeutet nur: Ein relevanter Zustand hat sich geändert. Sie garantiert nicht, dass ein bestimmter Thread seine Arbeit sofort fortsetzen darf.
Warum steht wait() in einer while-Schleife?
Angenommen, ein einelementiger Puffer ist voll und zwei Erzeuger möchten schreiben:
- Erzeuger A findet den Puffer voll und wartet.
- Ein Verbraucher entnimmt das Element und ruft
notifyAll()auf. - Erzeuger A und Erzeuger B können nun um den Monitor konkurrieren.
- Erzeuger B erhält ihn zuerst und füllt den Puffer wieder.
- Erzeuger A erhält den Monitor erst danach.
Mit if würde A nach dem Aufwachen ohne erneute Prüfung weitermachen. Mit while erkennt A, dass der Puffer inzwischen wieder voll ist, und wartet erneut.
Eine Benachrichtigung verspricht keine erfüllte Bedingung. Prüfe die Bedingung nach jedem Aufwachen erneut.
Wähle in jeder Lücke die passende Form und prüfe anschließend deine Antworten.
Eine Monitorlösung prüft die Wartebedingung mit einer `-Schleife. wait() gibt die frei. Nach notifyAll() müssen geweckte Threads ihre ` erneut prüfen.
while schützt vor inzwischen wieder veränderten Zuständen. Das Freigeben der Monitorsperre ermöglicht der Gegenseite die nötige Änderung; die Bedingung entscheidet anschließend, wer wirklich fortfahren darf.while schützt vor inzwischen wieder veränderten Zuständen. Das Freigeben der Monitorsperre ermöglicht der Gegenseite die nötige Änderung; die Bedingung entscheidet anschließend, wer wirklich fortfahren darf.Polling oder blockierendes Warten?
Beim Polling fragt ein Thread denselben Zustand ständig erneut ab. Das kann Rechenzeit verbrauchen. Wartet er dabei sogar mit gehaltener Sperre, verhindert er möglicherweise die Zustandsänderung selbst.
Blockierendes Warten setzt den Thread dagegen aus, bis eine Benachrichtigung erfolgt. Vorgefertigte Klassen wie eine beschränkte BlockingQueue kapseln diese Koordination: put wartet bei einer vollen Queue, take bei einer leeren.
Karteikasten
Überlege zuerst selbst und drehe die Karte anschließend zum Prüfen um.
Alles auf einen Blick
- Erzeuger-Verbraucher-Problem
- Gemeinsamer Puffer
- leer: Verbraucher wartet
- voll: Erzeuger wartet
- Sicherer Zugriff
- Mutex schützt Veränderungen
- kritischer Abschnitt bleibt kurz
- Semaphore
- `sem_read` zählt Elemente
- `sem_write` zählt Plätze
- Java-Monitor
- `wait()` gibt Sperre frei
- `while` prüft erneut
- Ziel
- keine ungültigen Pufferzustände
- kein unkontrolliertes Warten
- Gemeinsamer Puffer
Abschluss-Check
Wenn du alle drei Fragen begründen kannst, kannst du die beiden Ebenen einer Lösung auseinanderhalten: Zähl- oder Wartebedingungen regeln, wann eine Operation erlaubt ist; der gegenseitige Ausschluss schützt, wie der Puffer verändert wird.
Mit Google fortfahren