Informatik IV 37-004 SS 2002 Thomas Gross Diese Slides kommen ohne Gewähr. 9-1 Thomas Gross 1997-2000 Nebenläufige Programmierung in Java Threads und deren Zustände Speichermodell Einfache Synchronisation 9-2 Thomas Gross 1997-2000 Page 1
Konzept Threads Threads stehen in Java für das was wir Prozess in unseren bisherigen Überlegungen zur Nebenläufigkeit genannthaben Wir brauchen ein Modell, um die Ausführungen von Programmen in einem parallelen System zu beschreiben. Wir wollen uns nicht genau um die Anzahl der CPUs kümmern Unsere Ergebnisse sollen für alle möglichen Systeme gelten, nicht nur für ein bestimmtes Wir nennen das Konzept der Ausführung einer Folge von Operationen einen Thread. Ein Thread weiss, welche Operation als nächste auszuführen ist. 9-3 Thomas Gross 1997-2000 Threads Ein Thread braucht einen Arbeitsspeicher (Adressraum), um Daten und Zwischenergebnisse zu speichern. Adressraum des Threads: alle Objekte, die via Verweise erreichbar sind. Ein Adressraum kann mehreren Threads dienen. Java: Lokale Variable und Methodenparameter sind nur für den aufrufenden Thread sichtbar (Laufzeitstack) Klassenvariable oder mittels new erzeugte Objekte (Heap) sind im Prinzip für alle Threads sichtbar 9-4 Thomas Gross 1997-2000 Page 2
Speichermodell Ähnlich shared memory Thread 1 + lokaler Speicher Thread 2 + lokaler Speicher Thread 3 + lokaler Speicher Gemeinsamer Speicher Bemerkung: Threads werden vom Scheduler auf tatsächlich vorhandene CPUs abgebildet. 9-5 Thomas Gross 1997-2000 Threads und Prozessoren Ein Thread kann von jeder CPU im System ausgeführt werden Ein Thread kann nach jeder Operation unterbrochen werden Er kann auf einer anderen CPU weiter ausgeführt werden Er kann für eine Zeit aussetzen Wir können mit diesem Modell auch die Ausführung mehrerer Threads auf einer CPU beschreiben 9-6 Thomas Gross 1997-2000 Page 3
Quasi-Parallel Mehrere Threads werden auf einer CPU ausgeführt. Wir wissen nicht, wann Unterbrechungen stattfinden ( nach jeder Operation möglich ) Wir sagen, die Threads werden quasi -parallel ausgeführt Unsere Programme sollen für alle möglichen Ausführungen korrekt sein -> Konzeptionell kein Unterschied zur echten Parallelität (n CPUs) -> Allgemein: Nebenläufige Java Programme 9-7 Thomas Gross 1997-2000 Threads Threads in Java eingebaut. Jedes Java Programm wird durch eine Anzahl von Threads ausgeführt. Garbage Collector ist eigener Thread AWT Thread Zusätzlich können vom Benutzer Threads definiert, gestartet, und gemanaged werden. 9-8 Thomas Gross 1997-2000 Page 4
Threads und Prozessoren Die genauen Regeln wann ein Thread eine CPU bekommt (und wann sie wieder abgegeben wird) hängen von der Implementation des Systemes ab. Insbesondere können wir nicht Fairness voraussetzen. Programm sollte sich nicht auf Fairness verlassen» Trotzdem muss es unsere Anforderungen erfüllen Unterschiedliche Ergebnisse auf verschiedenen Platformen (oder Versionen) möglich! 9-9 Thomas Gross 1997-2000 Threads sind Objekte Alle Aussagen für Object gelten auch für Threads. Thread t = new Thread(); Threads können verschiedene Zustände annehmen: Instanziert (der Thread existiert)» d.h. es wurde der Konstruktor ausgeführt. Ausführbar (der Thread kann die CPU besetzen, so dass Instruktionen ausgeführt werden können) -- evtl. hat der Thread sogar die CPU. Suspended (der Thread kann z.zt. nicht ausgeführt werden, z.b. weil er wartet) Dead (der Thread ist nicht mehr aktiv, aber die Felder können noch inspiziert [und verändert] werden)» Threads sind Objects.» Es gibt auch Konstruktoren von Thread, die einen anderen Thread als Parameter akzeptieren De-alloziert (dem Garbage Collector überlassen) 9-10 Thomas Gross 1997-2000 Page 5
Thread Management Die start() Methode macht einen Thread ausführbar Wann der Thread ausgeführt wird, hängt vom System (u.a. den anderen Threads) ab. Siehe Bemerkung über Zeitpunkt des Besitzes der CPU Beispiel Thread t = new Thread(); t.start(); Die Methode start() verwaltet nur den Thread, sofortiger Rückkehr (nachdem die Buchhaltung up-to-date ist). Offene Frage: Was führt der Thread aus? 9-11 Thomas Gross 1997-2000 Zustände new this.yield instanziert this.start ausführbar this.sleep, other.join, obj.wait, block on synchronized ende run methode this.interrupt, obj.notify[all], enter synchronized suspended de-alloziert dead Keine Referenz auf das Thread Objekt existiert mehr this ~ aktueller Thread other ~ anderer Thread obj ~ irgendein Objekt Zustandsübergänge nicht vollständig 9-12 Thomas Gross 1997-2000 Page 6
Prioritäten Cooperative Threads: Alle Threads haben gleiche Priorität Thread gibt freiwillig Ausführungsrecht ab (Methoden yield() oder sleep()) Premptive Threads: Threads können unterschiedliche Prioritäten haben (setpriority(), getpriority()) Scheduler unterbricht Threads (mit niedriger Priorität, regelmässig) -> nicht genau festgelegt in der Java Spezifikation -> Programmierer sollte sich niemals auf Scheduler bzw. zeitliches Verhalten verlassen 9-13 Thomas Gross 1997-2000 Verhalten eines Threads Jede Instanz der Klasse Thread führt eine vorgegebene Methode aus Signatur ist festgelegt Name: run() run() ist eine parameter-lose Methode, die ausgeführt wird, nachdem der Thread aktiviert wurde (mittels start()). Expliziter Aufruf von run() nur wenn wir auf den Return warten wollen (oder müssen). Sofortiger Return von Methode start() Strategie Muster (abstrakte Strategieklasse ist Runnable) 9-14 Thomas Gross 1997-2000 Page 7
Strategie: Thread Aktivität Threads implementieren das Interface Runnable. interface Runnable { void run(); Festlegen des Verhaltens durch Erweiterung der Thread Klasse (Neu)Definition vonrun() class MyThread extends Thread { public void run() { // actual code 9-15 Thomas Gross 1997-2000 class PingPong extends Thread { String word; int delay; PingPong (String msg, int delaytime) { word = msg; delay = delaytime; public void run () { try { for (; ; ) { System.out.println(word + " " ); sleep (delay); catch (InterruptedException e) { return; 9-16 Thomas Gross 1997-2000 Page 8
(2.Teil) public class TrivialApplication { public static void main(string args[]) { new PingPong("foo", 5000).start(); new PingPong("bar", 2500).start(); System.out.println( "Done" ); Output: Done foo bar bar foo bar foo bar bar bar 9-17 Thomas Gross 1997-2000 Unteilbare Operationen Nochmals ein Blick auf das Speichermodell. Sind Speicherzugriffe atomar? Wann sieht ein Thread Veränderungen die ein anderer Thread durchführte? Sehen alle Threads alle Speicherzugriffe in der gleichen Reihenfolge? Als Benutzer einer Programmiersprache sehen wir im Language Reference Manual nach... wenn wir Glück haben gibt es da eine Auskunft! In Java sind diese Aspekte relativ genau festgelegt...... andere Programmiersprachen überlassen die Entscheidung dem Entwickler des Compilers/Laufzeitsystems... oder denken garnicht an diesen Problembereich 9-18 Thomas Gross 1997-2000 Page 9
Java: Speicherzugriffe Nebenläufiger Zugriff auf Basistypen (ausser double und long) (1) Kein Korrumpieren von Daten: Schreib- bzw. Lesevorgang nicht unterbrechbar (atomar) (2) Keine Garantie bzgl. der Ausführungsreihenfolge für Zugriffe auf unterschiedliche Variable innerhalb eines Threads. (3) Keine Garantie bzgl. der Reihenfolge, in der Schreibzugriffe auf gemeinsame Variable für andere Threads sichtbar werden Java: Fürdouble und long, Arrays und Referenztypen gilt (1) nicht! 9-19 Thomas Gross 1997-2000 Konsistenz: Speicherbild Problem: Alle Threads sollen ein konsistentes (= übereinstimmendes) Speicherbild haben. Nicht selbstverständlich: Vorhandensein von threadlokalen Caches / Registern, Umordnen von Instruktionen durch den Compiler Variablen könnenvolatile deklariert werden: Dann hat man stärkere Garantien für Atomizität, Sichtbarkeit und Reihenfolge als auf dem letzten Slide erklärt. volatile hilft nicht um Speicherbild von ganzen Arrays oder Objekten konsistent zu halten Also: alle Variablen, die von mehreren Threads gelesen oder geschrieben werden, sollten volatile sein. 9-20 Thomas Gross 1997-2000 Page 10
Konsistenz: Speicherzustand Problem: Gemeinsamer Speicher kann von mehreren Threads verändert werden. Selbst bei konsistentem Speicherbild kann es zu inkonsistentem Speicherzustand kommen: Wir müssen aufpassen, dass die Änderungen in der richtigen Reihenfolge bzw. nach gewissen Regeln passieren Explizite Kontrolle der Abfolge von Operationen (= Kontrollfluss) unterschiedlicher Threads: Synchronisation Beispiel: Lost-Update Problem 9-21 Thomas Gross 1997-2000 Lost update class Account { volatile int balance = 1; void transaction(int i) { balance = balance + i; // not atomic! int getbalance() { return balance; Problem falls mehrere Threads auf der gleichen Instanz von Account die Methode transaction ausführen! 9-22 Thomas Gross 1997-2000 Page 11
int c1 = 1; // global int c2 = 1; int turn; Dekker s Lösung Prozess 1: // non-critical section c1 = 0; while (!(c2 == 1)){ if (turn == 2) { c1 = 1; while (! (turn==1)){ c1 = 0; // critical section c1 = 1; turn = 2; Prozess 2: // non-critical section c2 = 0; while (!(c1 == 1)) { if (turn == 1) { c2 = 1; while (! (turn==2)){ c2 = 0; // critical section c2 = 1; turn = 1; 9-23 Thomas Gross 1997-2000 class Dekker { volatile int c1 = 1; volatile int c2 = 1; volatile int turn; class Thread1 extends Thread { Dekker d_; Thread1 (Dekker d) {d_ = d; Dekker slösung class Thread2 extends Thread { Dekker d_; Thread2 (Dekker d) {d_ = d; public void run() { public void run() { // non-critical section // non-critical section d_.c1 = 0; d_.c2 = 0; while (!(d_.c2 == 1)){ while (!(d_.c1 == 1)) { if (d_.turn == 2) { if (d_.turn == 1) { d_.c1 = 1; d_.c2 = 1; while (!(d_.turn==1)){ while (! (d_.turn==2)){ d_.c1 = 0; d_.c2 = 0; // critical section // critical section d_.c1 = 1; d_.c2 = 1; d_.turn = 2; d_.turn = 1; 9-24 Thomas Gross 1997-2000 Page 12
Dekker s Lösung class Test { public static void main(string[] args) { Dekker d = new Dekker(); new Thread1(d).start(); new Thread2(d).start(); System.out.println("Done."); 9-25 Thomas Gross 1997-2000 Bakery Algorithmus Idee: jeder Kunde nimmt ein Ticket mit einer Zahl Wenn ein Verkäufer frei wird, wird der Kunde mit dem Ticket mit der kleinsten Nummer bedient Vorteil: es gibt keine Variable, die von allen Prozessen gelesen und geschrieben werden muss. 9-26 Thomas Gross 1997-2000 Page 13
int c1 = 0; // global int c2 = 0; Bakery - 2 Prozesse Prozess 1: // non-critical section c1 = 1; c1 = c2 + 1; while (!( (c2 == 0) (c1 <= c2)) ){ // critical section c1 = 0; Prozess 2: // non-critical section c2 = 1; c2 = c1 + 1; while (!( (c1 == 0) (c2 < c1)) ){ // critical section c2 = 0; 9-27 Thomas Gross 1997-2000 Bakery Algorithmus Garantiert gegenseitigen Ausschluss Verhindert Deadlock 9-28 Thomas Gross 1997-2000 Page 14
Bakery - N Prozesse int [] c = new int [N+1]; // c[0] not used! init to 0 int [] n = new int [N+1]; // n[0] not used! init to 0 // max(n) : maximum number in n Prozess i: // non-critical section c[i] = 1; n[i] = 1 + max(n); c[i] = 0; for (int ti=1; ti<n+1; ti++) { if (ti!= i) { while (!(c[ti] == 0)){ while (! ((n[ti] == 0) (n[i] < n[ti]) ((n[i] == n[ti]) && (i < ti)) )) { // critical section n[i] = 0; 9-29 Thomas Gross 1997-2000 Andere Modelle Trotzdem sind diese Lösungen nicht besonders elegant Wir wollen nicht unseren Code mit diesen Protokoll Operationen durchsetzen Ein Prozess der auf Zugang zu seiner critical section wartet verbraucht (Prozessor) Ressourcen 9-30 Thomas Gross 1997-2000 Page 15
Semaphore Vereinfachung des Protokolls Operationen die einem Prozess es erlauben, die critical section auszuführen Idee: EntryTest() --» Erfolg (d.h. Return): Critical Section kann ausgeführt werden» Noch kein Return: Warten ExitNotification() -- ein Prozess verlässt die Critical Section» Andere Prozesse duerfen weiter machen 9-31 Thomas Gross 1997-2000 Semaphore Idee: diese EntryTest() Methoden werden auf Objekte angewendet die einen Integer Wert W haben Wert immer >= 0 Wenn Wert W > 0 dann: EntryTest () reduziert W um 1 (W = W - 1) und lässt den Prozess weiter» Sonst muss der Prozess warten ExitTest(): wenn andere Prozesse warten, dann wird einer aktiviert, sonst wird W um 1 erhöht Diese Objekte nennen wir Semaphore Sei S ein Semaphor. S.p() -- EntryTest S.v() -- ExitNotification 9-32 Thomas Gross 1997-2000 Page 16
P() und V() Operationen P() und V() werden atomar ausgeführt Ein Semaphor kann mit einem beliebigen (nichtnegativem) Wert initialisiert werden. 9-33 Thomas Gross 1997-2000 Beispiel: Gegenseitiger Ausschluss Semaphore sema = new Semaphore(); sema.value = 1; // init Prozess 1: // non-critical section sema.p(); // critical section sema.v(); Prozess 2: // non-critical section sema.p(); // critical section sema.v(); 9-34 Thomas Gross 1997-2000 Page 17
Invarianten Nach unserer Definition gilt immer für einen Semaphor sema: sema.value >= 0 sema.value = sema.init_value + #Vs - #Ps Nur ein Prozess, der erfolgreich sema.p() durchgeführt hat, kann eine exitnotification Operation (sema.v()) ausführen. 9-35 Thomas Gross 1997-2000 Verhalten bei V() Nachdem ein Prozess sema.v() ausgeführt hat, wird ein anderer Prozess, der auf sema wartete, aktiviert. Frage: wenn es mehr als einen gibt, welcher Prozess wird ausgeführt? Antwort: Hängt ab von der Art/Variation des Semaphors. 9-36 Thomas Gross 1997-2000 Page 18
Producer-Consumer Grundmuster verschiedener Anwendungen Producer P --> Consumer C Geschwindigkeit von P und C zeitweilig unterschiedlich Buffer zwischen P und C Zuerst: unendlicher Buffer 9-37 Thomas Gross 1997-2000 int [] b; int in, out; Unendlicher Buffer Prozess P: int j; produce(j); b[in] = j; in++; Prozess C: int j; < await (in > out) j = b[out]; out ++; > consume(j); <await (B) S;> kann implementiert werden durch -- Lesen eines Zaehlers (mit Dekker s Lösung) -- Semaphore 9-38 Thomas Gross 1997-2000 Page 19
int [] b; int in, out; Semaphore access = new Semaphore(); access.value = 0; Synchronization Prozess P: int j; produce(j); b[in] = j; in++; access.v(); Prozess C: int j; access.p(); j = b[out]; out ++; consume(j); 9-39 Thomas Gross 1997-2000 int [] b = new int[n]; int in, out; Semaphore access = new Semaphore(); access.value = 0; Semaphore space = new Semaphore(); space.value = N; Endlicher Buffer Prozess P: int j; produce(j); space.p(); b[in] = j; in = mod(in+1,n); access.v(); Prozess C: int j; access.p(); j = b[out]; out = mod(out+1,n); space.v(); consume(j); 9-40 Thomas Gross 1997-2000 Page 20
Mehrere Buffer Wenn es mehrere Buffer gibt, dann kann es auf subtile Weise ein Deadlock geben. 9-41 Thomas Gross 1997-2000 int [] b1 = new int[n]; int [] b2 = new int[n]; int in, out; Semaphore access = new Semaphore(); access.value = 0; Semaphore space = new Semaphore(); space.value = N; Mehrere Buffer (2) Prozess P: int j1,j2; // produce(j1, j2); space.p(); b1[in] = j1; b2[in] = j2; in = mod(in+1,n); access.v(); Prozess C: int j1, j2; access.p(); j1 = b1[out]; j2 = b2[out]; out = mod(out+1,n); space.v(); // consume(j1, j2); 9-42 Thomas Gross 1997-2000 Page 21
int [] b1 = new int[n1]; int [] b2 = new int[n2]; int in1, in2, out1, out2; Semaphore access1, access2, space1, space2; // create semaphores access1.value = 0; access2.value = 0; space1.value = N1; space2.value = N2; Prozess P: int j1,j2; // K1 elements in b1 for (int k=0; k<k1; k++) { produce(j1); space1.p(); b1[in] = j1; in1 = mod(in1 + 1,N1); access1.v(); Mehrere Buffer (3A) Prozess C: int j1, j2; // K2 elements from b2 for (int k=0; k<k2; k++) access2.p(); j2 = b2[out2]; out2 = mod(out2+1,n); space2.v(); consume(j2); 9-43 Thomas Gross 1997-2000 Mehrere Buffer (3B) // Fortsetzung von Buffer (3A) // K2 elements in b2 for (int k=0; k<k2; k++) { produce(j2); space2.p(); b2[in2] = j2; in2 = mod(in2 + 1,N2); access2.v(); // end of while // // K1 elements from b1 for (int k=0; k<k1; k++) { access1.p(); j1 = b1[out1]; out1 = mod(out1+1,n); space1.v(); consume(j1); // end of while Beispiele N1 = 100, N2 = 100, K1 = 10, K2 = 10 N1 = 100, N2 = 100, K1 = 101, K2 = 10 Programmieren mit Semaphoren trickreich. Es kann schwer sein, die Logik der Synchronization zu erkennen. 9-44 Thomas Gross 1997-2000 Page 22
Fortsetzung 9-45 Thomas Gross 1997-2000 Binäre Semaphore Wenn der Semaphore nur die Werte 0 oder 1 annehmen kann, dann sprechen wir von einem binären Semaphor. Interessant weil oft leicht zu realisieren. 9-46 Thomas Gross 1997-2000 Page 23
Weitere Informationen Es gibt eine Reihe von Büchern über die Themen dieser Vorlesung. M. Ben-Ari Principles of Concurrent and Distributed Programming, Prentice Hall, 1990 (ISBN: 0-13-711821) G. Andrews Foundations of Multhithreaded, Parallel, Distributed Programming, Addison Wesley, 2000 (ISBN: 0-201-35752-6) 9-47 Thomas Gross 1997-2000 Page 24