Parallele Programmierung (3IB)
=> Grundlage: Hettel und Tran (2016, Kap. 5 und Abschnitt 8.1.3)
synchronized
synchronized
und einem Warteraum
“Eine Menge von Prozessen befindet sich in einem Deadlock-Zustand, wenn jeder Prozess aus der Menge auf ein Ereignis wartet, das nur ein anderer Prozess aus der Menge auslösen kann.”1
Ein Prozess “verhungert”, wenn er nie Zugriff auf eine Ressource erhält, die von mehreren Prozessen beansprucht wird.
Eine Menge Prozessen befindet sich in einem Livelock-Zustand, wenn mindestens ein Prozess verhungert, weil die Kontrolle zwischen anderen Prozessen hin und her geht, ohne dass verhungernde Prozesse die Kontrolle jemals mehr erlangen können. (Kein Deadlock!)
Problemsituation
Lösung (eine von mehreren möglichen): Prioritätsvererbung
Beispiel: Pathfinder (NASA Mars-Fahrzeug, 1996)1
Die Bedeutung im Java-Umfeld ist gering oder zumindest unklar, da kein spezifisches Scheduling Verfahren für JVM vorgegeben ist.
synchronized
synchronized
)synchronized
synchronized
in privater Implementierung einer Methode in externer Klasse - z.B. Library)static Object l1 = new Object();
static Object l2 = new Object();
static void m1() {
synchronized (l1) {
synchronized (l2) {
/* ... */
}
}
}
static void m2() {
synchronized (l2) {
synchronized (l1) {
/* ... */
}
}
}
public static void main(String... args) {
var t1 = new Thread(Deadlock::m1); t1.start();
var t2 = new Thread(Deadlock::m2); t2.start();
}
pp.03.01-SynchPhilosopher
Der Lebenszyklus eines/r Philosoph:in ist:
Greifen von Esstäbchen ist kritischer Abschnitt
Wenn alle Threads auf die Locks in derselben numerischen Reihenfolge zugreifen (in den synchronized
-Block eintreten), dann kommt es nicht zu einem Deadlock (“global ordering rule”).
Die meisten Philosophen folgen diesem Schema: left
und right
sind Chopstick
-Objekte. Philosoph \(i\) und Philosoph \(i+1\) teilen ein Chopstick
-Objekt. Für Philosoph \(i\) ist das das rechte, auf das als Zweites zugegriffen wird, für Philosoph \(i+1\) ist das das linke, auf das als Erstes zugegriffen wird.
Nur im Fall des letzten bzw. 0. Philosophs wird diese Reihenfolge umgekehrt, daher kann es zum Deadlock kommen.
“To my mind, what makes multithreaded programming difficult is not that writing it is hard, but that testing it is hard. It’s not the pitfalls that you can fall into; it’s the fact that you don’t necessarily know whether you’ve fallen into one of them.”1
synchronized
und einem Warteraumwait
+ notify
/notifyAll
)Manchmal ist es erforderlich Threads über Ereignisse zu steuern.
Dazu dient ein der Monitor eines “Vermittlerobjekts”. Es muss lediglich von Object
erben. Die beteiligten Threads kommunizieren über den Monitor dieses Vermittlers miteinander.
Ein Thread muss im Besitz des Monitors des Vermittlers sein, um die Methoden wait()
, notify()
und notifyAll()
aufrufen zu dürfen, sonst wird eine IllegalMonitorStateException
geworfen.
Diese Methoden können somit nur innerhalb von synchronized
-Blöcken bzw. -Methoden verwendet werden.
Alle wartenden Threads sind in der condition queue des Locks, auch condition variable oder Warteraum genannt.
obj.wait()
:
t1
prüft das Vorhandensein einer Wartebedingung (ist in der Regel kritischer Abschnitt, daher in synchronized
-Block).t1
muss spätestens jetzt den Monitor (“Schloss”) des vermittelnden Lock-Objekts obj
belegt haben.t1
in die Warteliste des vermittelnden Lock-Objekts obj
ein.t1
gibt den Monitor von obj
wieder frei.t1
wechselt in den WAITING
-Zustand und bleibt so lange darin, bis ihn ein anderer Thread (im Folgenden t2
) durch obj.notify()
oder obj.notifyAll()
weckt oder er ein t1.interrupt()
erhält.obj.notifyAll()
:
t2
muss den Monitor des vermittelnden Lock-Objekts obj
belegt haben.obj
warten (neuer Zustand RUNNABLE
).obj
wieder, bevor er mit den Anweisungen nach wait()
fortfährt. Daher kann nur einer der wartenden Threads fortfahren, obwohl alle aufgeweckt wurden.obj.notify()
:
t2
muss den Monitor des vermittelnden Lock-Objekts obj
belegt haben.obj
wartet, und weckt ihn auf (neuer Zustand RUNNABLE
).obj
wieder, bevor er mit den Anweisungen nach wait()
fortfährt.wait
+ notify
/notifyAll
)synchronized(obj) {
while (/* «Wartebedingung» */) {
obj.wait();
}
/* «Benutzung geteilter Ressourcen» */
obj.notifyAll();
}
while
: Falls in der Zwischenzeit die Bedingung wieder eingetreten ist oder der Thread “fehlerhaft” geweckt wurde; if
statt while
wäre hier ein Fehler!notifyAll
: Falls mehrere Threads auf unterschiedliche Bedingungen an obj
warten, muss zum Signalisieren immer notifyAll()
benutzt werden!Start:
items.length |
8 |
Kapazität von items |
count |
0 |
aktuelle Anzahl Elemente |
takeptr |
0 |
Index nächstes take() |
putptr |
0 |
Index nächstes put() |
count == 0
)take()
-Aufrufe warten.put(42)
:
items.length |
8 |
Kapazität von items |
count |
1 |
aktuelle Anzahl Elemente |
takeptr |
0 |
Index nächstes take() |
putptr |
1 |
Index nächstes put() |
42
ist an Index 0
von items
items
put(99)
:
items.length |
8 |
Kapazität von items |
count |
2 |
aktuelle Anzahl Elemente |
takeptr |
0 |
Index nächstes take() |
putptr |
2 |
Index nächstes put() |
99
wurde als nächstes gespeichertput(80)
:
items.length |
8 |
Kapazität von items |
count |
3 |
aktuelle Anzahl Elemente |
takeptr |
0 |
Index nächstes take() |
putptr |
3 |
Index nächstes put() |
80
wurde als nächstes gespeichertput(12)
:
items.length |
8 |
Kapazität von items |
count |
4 |
aktuelle Anzahl Elemente |
takeptr |
0 |
Index nächstes take() |
putptr |
4 |
Index nächstes put() |
12
wurde als nächstes gespeicherttake()
\(\to\) 42
:
items.length |
8 |
Kapazität von items |
count |
3 |
aktuelle Anzahl Elemente |
takeptr |
1 |
Index nächstes take() |
putptr |
4 |
Index nächstes put() |
put(20)
:
items.length |
8 |
Kapazität von items |
count |
4 |
aktuelle Anzahl Elemente |
takeptr |
1 |
Index nächstes take() |
putptr |
5 |
Index nächstes put() |
20
wird gespeichert und putptr
wird weiterbewegttakeptr
bleibt unverändertput(14)
:
items.length |
8 |
Kapazität von items |
count |
5 |
aktuelle Anzahl Elemente |
takeptr |
1 |
Index nächstes take() |
putptr |
6 |
Index nächstes put() |
14
wird gespeichert und putptr
wird weiterbewegttakeptr
bleibt unverändertput(44)
:
items.length |
8 |
Kapazität von items |
count |
6 |
aktuelle Anzahl Elemente |
takeptr |
1 |
Index nächstes take() |
putptr |
7 |
Index nächstes put() |
44
wird gespeichert und putptr
wird weiterbewegttakeptr
bleibt unverändertput(75)
:
items.length |
8 |
Kapazität von items |
count |
7 |
aktuelle Anzahl Elemente |
takeptr |
1 |
Index nächstes take() |
putptr |
0 |
Index nächstes put() |
75
wird gespeichert und putptr
wird weiterbewegttakeptr
bleibt unverändertput(13)
:
items.length |
8 |
Kapazität von items |
count |
8 |
aktuelle Anzahl Elemente |
takeptr |
1 |
Index nächstes take() |
putptr |
1 |
Index nächstes put() |
items.length == count
)put()
-Operationen müssen wartentake()
\(\to\) 99
:
items.length |
8 |
Kapazität von items |
count |
7 |
aktuelle Anzahl Elemente |
takeptr |
2 |
Index nächstes take() |
putptr |
1 |
Index nächstes put() |
take()
entfernt Element an Index 1
items.length > count
)put(22)
:
items.length |
8 |
Kapazität von items |
count |
8 |
aktuelle Anzahl Elemente |
takeptr |
2 |
Index nächstes take() |
putptr |
2 |
Index nächstes put() |
count == items.length
put()
muss wartencount == 0
take()
muss wartenwait
/notifyAll
für Ringpuffer”pp.03.02-BoundedQueueWaitNotify
synchronized
vorhanden, auf der wait()
/notify()
bzw. notifyAll()
ausgeführt werden kann.notifyAll()
aufgeweckt werden, kann es sein, dass Verarbeitungskapazizät vergeudet wird. Mit Condition
können mehrere getrennte “Warteräume” etabliert werden.Condition
aus dem Package java.util.concurrent.locks
) in einem “Monitor” zu verwenden.synchronized
muss dann aber das Interface Lock
aus dem Package java.util.concurrent.locks
benutzt werden, um den Monitor zu begrenzen.Lock
)Condition
)import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.Condition;
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
lock.lock();
try {
while (/* «Wartebedingung» */) {
condition.await();
}
/* «Benutzung geteilter Ressourcen» */
} finally {
lock.unlock();
}
Condition
)Wenn die wartenden Threads nun “bedingungsgenau” in getrennten Warteräumen (für mehrere Bedingungsvariablen) verweilen, kann signal()
statt signalAll()
zum Aufwecken benutzt werden, da sichergestellt ist, dass ein zufällig ausgewählter aufgeweckter Thread eine Situation vorfindet, in der die Bedingung, auf die gewartet wurde, erfüllt ist.
Würden mehrere Threads auf unterschiedliche inhaltliche Bedingungen in demselben Warteraum warten, könnte es sein, dass ein Thread geweckt wird, der auf eine Bedingung wartet, die noch nicht eingetreten ist – er ruft daher await()
für sich auf. Damit wären alle Threads dann im Zustand WAITING
und es gäbe keine Möglichkeit, dass einer andere wecken könnte.
Condition
aus dem Package java.util.concurrent.locks
import java.util.Date;
import java.util.concurrent.TimeUnit;
public interface Condition {
public void await() // analog wait()
throws InterruptedException;
public void awaitUninterruptibly();
public long awaitNanos(long nanos)
throws InterruptedException;
public boolean await(long time, TimeUnit unit)
throws InterruptedException;
public boolean awaitUntil(Date deadline)
throws InterruptedException;
public void signal(); // analog notify()
public void signalAll(); // analog notifyAll()
}
await
/signal
für Ringpuffer”