Zurück zum Inhaltsverzeichnis des Manuskripts verteilte Systeme

8.2 Koordinierung nebenläufiger Programme

8.2.1 Produzenten/Konsumenten-Problem

Problemstellung

Der vorliegende Abschnitt ergänzt die Ausführungen aus dem Abschnitt 1.3.3 (Threadsicherheit) und führt in die Konzepte ein, mit denen nebenläufig stattfindende Aktivitäten koordiniert werden können. Diese Konzepte sollen durch die Diskussion eines klassischen Programmierproblems verdeutlicht werden. Es handelt sich um das Produzenten/Konsumenten-Problem, das in der Literatur in unterschiedlichen Ausprägungen beschrieben wird.

In der hier verwendeten Form benutzen genau zwei voneinander unabhängig arbeitende Akteure ein gemeinsames Lager von fester Größe. Einer der beiden Akteure heißt Produzent, der andere Konsument. In einer ersten und noch sehr groben Version arbeiten beide ohne Unterbrechung und ohne absehbares Ende. Der Produzent erzeugt Artikel (einen nach dem andern) und legt sie einzeln im Lager ab, der Konsument entnimmt dem Lager Artikel (ebenfalls einen nach dem andern) und verbraucht sie. Dabei sind weder die Artikel noch die Vorgänge der Produktion und des Verbrauchens von Artikeln von Interesse.

Das Lager besteht aus endlich vielen (aus n) Lagerstellen, und zwar derart, dass in einer Lagerstelle jeweils genau ein Artikel Platz hat. Die Lagerplätze sind von 0 bis (n-1) durchnummeriert. Beide Akteure müssen wissen, wie weit das Lager jeweils gefüllt ist, bevor sie ablegend oder entnehmend darauf zugreifen. Deshalb wird ein Füllstandanzeiger z eingerichtet, der immer auf den anschaulich obersten der belegten Lagerplätze zeigen soll. Das heißt, dass z, wenn das Lager leer ist, den Wert -1 hat. Ist das Lager voll, dann hat z den Wert (n-1). Die folgende Grafik veranschaulicht den Sachverhalt:


Produzent und Konsument

Erster Lösungsansatz

Um einen ersten Ansatz einer Lösung des Produzenten/Konsumenten-Problems zu beschreiben, wird ein an der Sprache Java orientierter Pseudocode verwendet. Das Lager wird dabei als ein Array von Artikeln behandelt:

Start mit leerem Lager (z=-1) produzent() { | konsument() { while(true) { | while(true) { artikel=produziere(); | while(z==-1); // warte while(z==(n-1)); // warte | artikel=Lager[z]; z++; | z--; Lager[z]=artikel; | konsumiere(artikel); } | } } | }

Produzent und Konsument arbeiten nebenläufig, also unabhängig voneinander, und verwenden ein sehr einfaches Koordinierungsverfahren: Hat der Produzent einen Artikel erzeugt und will ihn im Lager ablegen, dann prüft er den Füllstandanzeiger um zu sehen, ob im Lager überhaupt Platz ist. Ist das Lager voll, dann wiederholt er diese Prüfung solange, bis Platz vorhanden ist. Ganz analog handelt der Konsument bei der Ablage eines Artikels und eines möglicherweise leeren Lagers.

Der erste Ansatz ist falsch

Dieser erste Ansatz zu einer Lösung des Produzenten/Konsumenten-Problems ist falsch. Der Fehler, und darauf ist im Abschnitt 1.3.3 (Threadsicherheit) bereits hingewiesen worden, liegt im nicht vorhersagbaren zeitlichen Ablauf der beiden Akteuere. Die folgende Situation macht dies deutlich:

Kritischer Abschnitt

Diese Fehlerquelle hat zu dem Begriff des kritischen Abschnitts geführt. Bei verteilten Systemen werden nebenläufig arbeitende Akteuere durch Threads, also durch Kontrollflüsse in Prozessen (siehe Abschnitt 1.3.1 (Programmabarbeitungen)) realisiert, und diesen liegen Programme zu Grunde. Unter einem kritischen Abschnitt versteht man einen Teil eines Programms, in dem es möglich ist, dass bei seiner Abarbeitung durch einen Thread auf Betriebsmittel, z.B. auf Variablen, zugegriffen wird, auf die auch andere Threads zugreifen können. Ein Programm kann mehrere kritische Abschnitte enthalten. Bei dem ersten Lösungsansatz des Produzenten/Konsumenten-Problems gibt es in jedem der beiden Programme einen solchen Abschnitt. Das Betriebsmittel, auf das Produzent und Konsument zugreifen können, sind der Füllstandanzeiger und das Lager. Es handelt sich um die folgenden Befehlsfolgen:

Beim Produzenten Beim Konsumenten ---------------- ---------------- while(z==n-1); while(z==-1); z=z+1; y=Lager[z]; Lager[z]=x; z=z-1;

Bearbeitet ein Thread Befehle eines kritischen Abschnitts, so sagt man anschaulich, er habe diesen Abschnitt betreten.

Gegenseitiger Ausschluss

Nebenläufig arbeitende Threads müssen in der Lage sein, sich gegenseitig vom Betreten eines kritischen Abschnitts abzuhalten. Hat ein Thread einen kritischen Abschnitt betreten, dann darf kein anderer Thread in den entsprechenden kritischen Abschnitt seines Programms gelangen. Um dies zu erreichen sind Sperrverfahren entwickelt worden. Der nächste Abschnitt benutzt einen Sperrmechanismus der Programmiersprache Java, um einen weiteren Ansatz zur Lösung des Produzenten/Konsumenten-Problems vorzustellen.



Zurück zum Inhaltsverzeichnis des Manuskripts verteilte Systeme