Musterlösungen
Laboraufgabe “Dinierende Philosoph:innen”
Das Problem der Verklemmung resultiert in der ursprünglichen Lösung aus der Verletzung der “global ordering rule”: Die Philosophen sind an einem runden Tisch angeordnet, der über Arrays repräsentiert wird.
chopsticksist ein Array, in demChopstick-Objekte gespeichert werden.philosophersist ein Array, in dem diePhilosopher-Objekte (erben vonThread) gespeichert werden.
Jeder Philosoph mit Index \(i\) im Array philosophers greift folgendermaßen auf die Essstäbchen zu:
- links:
chopsticks[i] - rechts:
chopsticks[i+1]
Da es aber nur so viele Essstäbchen wie Philosophen gibt, würde beim letzten Philosophen der Zugriff auf das rechte Essstäbchen fehlschlagen:
bzw.
Um die Kreiseigenschaft mit diesen Arrays abzubilden, wird für den Zugriff auf das rechte Essstäbchen modulo der Anzahl der Philosophen gerechnet:
- links:
chopsticks[i] - rechts:
chopsticks[(i + 1) % PHILOSOPHER_NUM]
Dadurch dreht sich beim Zugriff des “letzten Philosophen”, also dem mit Index PHILOSOPHER_NUM] die Reihenfolge des Zugriffs um: Zuerst wird auf das Essstäbchen mit Index PHILOSOPHER_NUM-1 zugegriffen, danach auf das Stäbchen mit Index 0. Die richtige Reihenfolge müsste aber wie bei den anderen Zugriffen der numerischen Reichenfolge folgen. Um dieses Problem zu beheben und sicher Verklemmungen zu vermeiden, muss daher beim letzten Philosophen also zuerst auf das rechte Essstäbchen und dann auf das linke zugegriffen werden.
Um dies zu erreichen, könnte die Methode eat() von Philosopher umgebaut werden. Da diese Methode aber von allen Philosophen verwendet wird (also auch von denen, bei denen die “reguläre” Reienfolge verwendet werden muss), würde diese Lösung recht unübersichtlich ausfallen.
Einfacher ist es, diese Methode unangetastet zu lassen und beim letzten Philosophen im Array nur umzudrehen, welches Stäbchen links und welches rechts liegt. Dazu muss die main-Methode von PhilosopherExperiment modifiziert werden:
public static void main(String... args) throws InterruptedException {
var chopsticks = new Chopstick[PHILOSOPHER_NUM];
var philosophers = new Philosopher[PHILOSOPHER_NUM];
for (int i = 0; i < PHILOSOPHER_NUM; i++) {
chopsticks[i] = new Chopstick();
}
// first n-1 philosophers
for (var i = 0; i < (PHILOSOPHER_NUM - 1); i++) {
philosophers[i] = new Philosopher(i, chopsticks[i],
chopsticks[(i + 1)]);
}
// nth philosophers: initialize with right+left instead of left+right
philosophers[PHILOSOPHER_NUM - 1] = new Philosopher(PHILOSOPHER_NUM - 1,
chopsticks[0], chopsticks[PHILOSOPHER_NUM - 1]);
for (var i = 0; i < PHILOSOPHER_NUM; i++) {
philosophers[i].start();
}
Thread.sleep(EXP_DURATION_MS);
for (var i = 0; i < PHILOSOPHER_NUM; i++) {
philosophers[i].stopPhilosopher();
}
}Laboraufgabe “wait/notifyAll für Ringpuffer”
Analyse des Quellcode
Analysieren Sie den Quellcode.
- Es handelt sich um einen Ringspeicher. Das Warten solange der Speicher leer ist (
take) bzw. solange der Speicher voll ist (put) ist aber noch nicht umgesetzt.
Laufverhalten
Lassen Sie das Hauptprogramm mehrfach laufen und beobachten Sie das Verhalten. Entspricht dies der Spezifikation?
- Dementsprechend kann es in Probeläufen dazu kommen, dass Elemente des Rinpuffers überschrieben werden, denn es wird aufgrund des Testtreibers (
main-Methode) seltener entnommen als hinzugefügt. Das zeigt sich dann daran, dass eigentlich vorhandene Elemente vontakenicht mehr entnommen werden können, da sie überschrieben wurden. Das zeigt die folgende Beispielausgabe:
Producer-1: put=1
Producer-2: put=1
Producer-2: put=2
Producer-2: put=3
Producer-2: put=4
Producer-2: put=5
Producer-2: put=6
Producer-2: put=7
Ab hier eigentlich voll, aber es geht weiter:
Producer-2: put=8
Dadurch wurde das erste Element “1” mit “8” überschrieben. Es wird auch entsprechend hier gelesen:
Consumer-1: taken=8
Modifikation von put und take, so dass gewartet wird, falls der Puffer voll ist
Modifizieren Sie die Methode put so, dass gewartet wird, falls der Puffer voll ist (this.count == this.items.lentgh). Dazu muss take alle wartenden Threads aufwecken, wenn ein Element entfernt wurde, weil im Puffer danach wieder etwas Platz ist.
solange der Speicher voll ist, warten:
Da
this.countbei jedem Hinzufügen um1erhöht und bei jedem Lesen um1verringert wird, kann man an einem Vergleich der Instanzvariablenthis.countmit der Größe des Container-Arraysthis.items(hier 8) feststellen, ob noch Platz im Speicher ist. In dem Fall muss der Aufrufer durchwait()blockiert werden.
Durch den Aufruf von wait() an this, also an der jeweiligen Instanz von BoundedBuffer wird der Warteraum von this verwendet. In diesem Warteraum werden die durch den Aufruf von put an einem bereits vollen Ringpuffer blockierten Threads warten.
Gibt es mehrere Instanzen von BoundedBuffer beeinflussen sie sich bzw. die Threads, die sie nutzen, nicht gegenseitig.
nun ist etwas Platz im Speicher: Alle wartenden Threads benachrichtigen:
Threads, die möglicherweise durch Aufruf von
wait()anthisblockiert sind, müssen nun aufgeweckt werden. Dies erfolgt durch den Aufruf vonnotifyAll()anthis. In diesem Beispiel würde es auch funktionieren, einen beliebigen wartenden Thread mitnotify()zu wecken, da es nicht passieren kann, dass anthisThreads sowohl auf die eine Bedingung (nicht mehr voll), als auch auf die anderen Bedingung (nicht mehr leer) warten. Es würde also mitnotify()immer ein auf die richtige Bedingung wartender Thread geweckt werden.synchronized:Der Aufruf von
wait()undnotify()/notifyAll()darf nur auf einem Bereich gegenseitigen Ausschluss geschehen. Deshalb werden die beiden Methodenputundtakemitsynchronizedgeschützt.
Modifikation von take und put, so dass gewartet wird, falls der Puffer leer ist
Modifizieren Sie die Methode take so, dass gewartet wird, falls der Puffer leer ist (items.count==0). Dazu muss put alle wartenden Threads aufwecken, wenn ein Element hinzugefügt wurde, weil im Puffer danach wieder etwas zu holen ist.
solange der Speicher leer ist, warten:
Da
countbeginnend mit 0 bei jedem Hinzufügen um 1 erhöht und bei jedem Lesen um 1 verringert wird, kann man an der Instanzvariablencountfeststellen, ob der Speicher leer ist. In dem Fall muss der Aufrufer durchwait()blockiert werden.
- nun ist etwas neues im Speicher: Alle wartenden Threads benachrichtigen:
Threads, die möglicherweise durch Aufruf von wait() an this blockiert sind, müssen nun aufgeweckt werden. Dies erfolgt durch den Aufruf von notifyAll() an this. In diesem Beispiel würde es auch funktionieren, einen beliebigen wartenden Thread mit notify()zu wecken, da es nicht passieren kann, dass an this Threads sowohl auf die eine Bedingung (nicht mehr voll), als auch auf die anderen Bedingung (nicht mehr leer) warten. Es würde also mit notify() immer ein auf die richtige Bedingung wartender Thread geweckt werden.
Laboraufgabe “await/signal für Ringpuffer”
Modifizieren Sie die Methoden put und take so, dass zwei getrennte Bedingungsvariablen verwendet werden.
- Statt
synchronizedwird hierReentrantLockfür den gegenseitigen Ausschluss verwendet, da mitnewConditionBedingungsvariablen zumLockerzeugt werden können. Es eergeben sich also die folgenden zusätzlichen Instanzvariablen mit Initialisierung:
- Die beiden Bedingungsvariablen sollen die beiden Situationen repräsentieren, in denen gewartet werden muss: “Speicher schon voll”, “Speicher noch leer”.
- Dementsprechend müssen die kritischen Regionen (hier die kompletten Rümpfe der Methoden
putundtake) mit gegenseitigem Ausschluss geschützt werden:
- Auf das Eintreten der jeweiligen Bedingung warten:
An den beiden Bedingungen ändert sich nichts. Lediglich das Warten (Blockieren des Aufrufer-Threads) erfolgt nun durch Aufruf vonawait()an jeweils einer der beiden Bedingungsvariablen.
bzw.
- Das Lösen der Blockierung geschieht hier durch Aufruf von
signal()(signalAll()ist im Allgemeinen nicht erforderlich, da durch Bedingungsvariablen immer “bedingungsgenau” gewartet werden kann). Der Aufruf muss “überkreuz” erfolgen. D.h. in der Methode, in der annotFullawait()aufgerufen wurde, musssignal()annotEmptyaufgerufen werden und umgekehrt.