Das Producer-Consumer-Problem (Erzeuger-Verbraucher-Problem, auch Bounded-Buffer-Problem) ist eines der klassischen Synchronisationsprobleme der Informatik. Eine oder mehrere erzeugende Threads stellen Daten her, eine oder mehrere verarbeitende Threads konsumieren sie — verbunden über einen gemeinsamen Puffer mit begrenzter Kapazität.
Das Problem
Der Puffer entkoppelt Erzeuger und Verbraucher zeitlich: Der Produzent kann weiterarbeiten, auch wenn der Konsument gerade beschäftigt ist, und umgekehrt. Dabei entstehen zwei Randbedingungen, die synchronisiert werden müssen:
- Voller Puffer: Ein Erzeuger darf nicht in einen vollen Puffer schreiben, sondern muss warten, bis ein Platz frei wird.
- Leerer Puffer: Ein Verbraucher darf nicht aus einem leeren Puffer lesen, sondern muss warten, bis Daten ankommen.
Ohne Synchronisation entstehen Wettlaufsituationen: Zwei Erzeuger können gleichzeitig in denselben Slot schreiben, zwei Verbraucher denselben Eintrag lesen.
Klassische Lösung
Die Standardlösung nutzt genau drei Primitive — einen Mutex für den Pufferzugriff und zwei Semaphore als Zähler:
semaphore empty = N // N freie Plätze
semaphore full = 0 // 0 belegte Plätze
mutex bufferLock
// Erzeuger:
produce(item)
empty.wait() // Platz reservieren
bufferLock.lock() // Puffer exklusiv
buffer[in] = item
bufferLock.unlock()
full.signal() // ein belegter Platz mehr
// Verbraucher:
full.wait() // auf Daten warten
bufferLock.lock() // Puffer exklusiv
item = buffer[out]
bufferLock.unlock()
empty.signal() // ein freier Platz mehr
consume(item)
empty zählt die freien Plätze (Erzeuger warten hier), full die belegten (Verbraucher warten hier). Wichtig ist die Praxisregel, nach dem Warten erneut in einer Schleife zu prüfen (while statt if), weil Threads auch ohne sichtbaren Grund aufwachen können (sogenannte spurious wakeups). Das entspricht der Mesa-Semantik von Bedingungsvariablen in Monitoren.
Einsatz in der Praxis
Das Muster steckt überall dort, wo Arbeit zwischen Threads oder Prozessen fließt:
- Unix-Pipes: Der Kernel-Puffer zwischen zwei Prozessen ist ein Bounded Buffer; Schreiben in eine volle Pipe blockiert den Erzeuger.
- Thread-Pools / Work Queues: Zentraler Server-Threads reihen Aufgaben in eine Warteschlange ein, Arbeiter-Threads holen sie ab.
- Message Queues: Kafka, RabbitMQ und ähnliche Systeme sind verteilte Producer-Consumer-Systeme.
- Logger: Anwendungs-Threads legen Log-Einträge in einen Puffer, ein dedizierter Thread schreibt sie gebündelt auf die Festplatte.
Bequeme Umsetzungen
- Go: Kanäle (
chan) kapseln Puffer und Synchronisation idiomatisch — ein buffered channel ist direkt ein Bounded Buffer. - Python:
queue.Queuemit festermaxsize;put()undget()blockieren automatisch korrekt. - Java:
BlockingQueue(etwaArrayBlockingQueue) mit blockierendenput()/take().
Varianten
Der Puffer kann unbegrenzt sein (kein empty-Zähler nötig, dafür wächst der Speicher) oder von mehreren Erzeugern und mehreren Verbrauchern gleichzeitig genutzt werden (Multi-Producer/Multi-Consumer, kurz MPMC). Beim Zugriff auf den gemeinsamen Puffer kommt statt des Mutex auch eine Lese-Schreib-Sperre in Betracht, wenn viele Leser gleichzeitig schauen sollen.
Verwandte Grundlagen: Semaphor, Mutex, Monitor, Nebenläufigkeit, Thread, kritischer Abschnitt.