Informatik

Erzeuger-Verbraucher-Problem einfach erklärt

Erzeuger-Verbraucher-Problem einfach erklärt
Erzeuger-Verbraucher-Problem einfach erklärt
Für Quiz, Lückentext, Lernkarten und Fortschritt ist JavaScript nötig. Alle Inhalte und Lösungen bleiben direkt lesbar.

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.

Deine Lernziele

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.

Definition

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.
Teste dich
Frage 1 von 1LeichtWelche Aussage beschreibt die Aufgabe des Puffers?
Lösung: Er speichert Elemente zwischen Erzeugung und Verarbeitung. — Der Puffer gleicht unterschiedliche Arbeitsgeschwindigkeiten nur innerhalb seiner Kapazität aus. Bei leerem oder vollem Puffer kann weiterhin Warten nötig 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.

Definition

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.

Beispiel

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.

Merke

Eine Sperre schützt wie der Puffer verändert wird. Eine Zustandsbedingung entscheidet, ob Einfügen oder Entnehmen gerade erlaubt ist.

Teste dich
Frage 1 von 1MittelEin Verbraucher hält die Puffersperre und wartet in einer gewöhnlichen Schleife auf ein Element. Warum ist das problematisch?
Lösung: Der Erzeuger kann die Sperre nicht erhalten und deshalb kein Element ablegen. — Wer auf eine Zustandsänderung durch die Gegenseite wartet, muss ihr den geschützten Zugriff ermöglichen. Bloßes Weiterprüfen bei gehaltener Sperre verhindert genau diese Änderung.
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:

SemaphorAnfangswertAufgabe
mutex1schützt die Veränderung des Puffers
sem_read0zählt entnehmbare Elemente
sem_writeNzä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:

Zustandsem_readsem_write
Start: leer04
nach einem Erzeuger13
nach einem weiteren Erzeuger22
nach einem Verbraucher13

Ablauf eines Erzeugers

  1. Das Element außerhalb des kritischen Abschnitts erzeugen.
  2. Mit P(sem_write) einen freien Platz reservieren.
  3. Mit P(mutex) den exklusiven Zugriff erwerben.
  4. Das Element ablegen und den Pufferzustand ändern.
  5. Mit V(mutex) den Zugriff freigeben.
  6. Mit V(sem_read) ein entnehmbares Element melden.

Ablauf eines Verbrauchers

  1. Mit P(sem_read) ein vorhandenes Element reservieren.
  2. Mit P(mutex) den exklusiven Zugriff erwerben.
  3. Das Element entnehmen und den Pufferzustand ändern.
  4. Mit V(mutex) den Zugriff freigeben.
  5. Mit V(sem_write) einen freien Platz melden.
  6. Das Element außerhalb des kritischen Abschnitts verarbeiten.
Gut zu wissen

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.

Teste dich
Frage 1 von 2MittelEin Puffer mit vier Plätzen war leer. Zwei Erzeugeroperationen und danach eine Verbraucheroperation wurden vollständig ausgeführt. Welche Zähler passen dazu?
Lösung: sem_read = 1 und sem_write = 3 — Es bleibt ein Element übrig. Deshalb gibt es ein entnehmbares Element und drei freie Plätze.
Frage 2 von 2SchwerWarum wäre die Reihenfolge P(mutex) vor P(sem_write) gefährlich, wenn der Puffer voll ist?
Lösung: Der Erzeuger könnte den Mutex halten und auf Platz warten, während der Verbraucher den Mutex zum Freimachen eines Platzes benötigt. — Ein voller Puffer kann nur durch einen Verbraucher wieder Platz erhalten. Hält der wartende Erzeuger bereits den Mutex, kann der Verbraucher den nötigen Zustand nicht herstellen.
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:

  1. Der aktuelle Thread wartet am Monitorobjekt.
  2. Er gibt dessen Sperre atomar frei.
  3. 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:

  1. Erzeuger A findet den Puffer voll und wartet.
  2. Ein Verbraucher entnimmt das Element und ruft notifyAll() auf.
  3. Erzeuger A und Erzeuger B können nun um den Monitor konkurrieren.
  4. Erzeuger B erhält ihn zuerst und füllt den Puffer wieder.
  5. 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.

Merke

Eine Benachrichtigung verspricht keine erfüllte Bedingung. Prüfe die Bedingung nach jedem Aufwachen erneut.

Lückentext

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.

Lösungen: Lücke 1: while; Lücke 2: Monitorsperre; Lücke 3: Bedingung. 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.

Teste dich
Frage 1 von 1SchwerEin Programm verwendet mehrere Erzeuger und Verbraucher an einem Monitor. Was ist nach notifyAll() die richtige Reaktion eines geweckten Threads?
Lösung: Er erwirbt den Monitor erneut und prüft seine eigene Bedingung in der Schleife. — Wecken und Fortsetzen sind verschiedene Schritte. Erst nach erneutem Sperrerwerb und erfolgreicher Bedingungsprüfung darf der Thread den Puffer verändern.
Karteikasten
Karteikasten

Überlege zuerst selbst und drehe die Karte anschließend zum Prüfen um.

Alles auf einen Blick
Mindmap
  • 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
Abschluss-Check
Teste dich
Frage 1 von 3LeichtWelche Aufgabe erfüllt mutex in der Semaphorlösung?
Lösung: Er verhindert gleichzeitig überlappende Änderungen am gemeinsamen Puffer. — mutex schützt den kritischen Abschnitt. Die zählenden Semaphore bilden dagegen vorhandene Elemente und freie Plätze ab.
Frage 2 von 3MittelEin Verbraucher soll ein Element entnehmen. Welche Reihenfolge ist richtig?
Lösung: Zuerst ein Element mit P(sem_read) reservieren, dann den Mutex erwerben, entnehmen, den Mutex freigeben und mit V(sem_write) einen Platz melden. — Der Verbraucher reserviert zuerst eine tatsächlich vorhandene Ressource. Erst dann betritt er kurz den kritischen Abschnitt und meldet nach der Entnahme den frei gewordenen Platz.
Frage 3 von 3SchwerEin Monitorpuffer ist leer. Ein Verbraucher wartet mit while (leer) wait(). Nach einer Benachrichtigung ist der Puffer beim erneuten Sperrerwerb wieder leer. Was muss geschehen?
Lösung: Der Verbraucher prüft die Bedingung erneut und wartet weiter. — Die Benachrichtigung zeigt nur eine mögliche Zustandsänderung an. Maßgeblich ist der Zustand in dem Moment, in dem der Verbraucher den Monitor wieder besitzt.

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.

Passend dazu