PARALLELE S YSTEME Ü BUNG 9, 18.06.08 Ü BERBLICK : ➜ Deadlocks ➜ Philosophenproblem ➜ Lösung mit Butler (Semaphor) ➜ Thread-Synchronisation und Java-Performance ➜ Deadlocks automatisch verhindern mit dem DeadlockDetectingLock Ü BUNG 9, 18.06.08 1 C OFFMAN , E LPHICK & S HOSHANI : D EADLOCK B EDINGUNGEN Vier Bedingungen: ➀ Serielle Benutzung von Resourcen (gegenseitiger Ausschluss) Z.B.: Monitore mit synchronized Methoden ➁ Inkrementelles Resourcen-Erlangen, d.h. während des Wartens auf eine Resource gibt der Prozeß die bereits erlangten Resourcen nicht frei Z.B.: Bei verschachtelten Monitoren behält Consumer den Lock, während er auf den Producer wartet ➂ Keine Preemption, d.h. dem Prozeß kann die bereits erlangte Resource nicht genommen werden Z.B.: Wird dem Consumer der Lock genommen, kann der Producer weiter arbeiten und somit gibt es kein Deadlock ➃ Wartezyklus: eine Prozeßkette existiert, wo jeder Prozeß die Resource hält, auf die sein Vorgänger wartet Z.B.: Consumer hält den Lock, den Producer benötigt; Producer hält das neue Symbol, den Consumer benötigt C OFFMAN , E LPHICK & S HOSHANI : D EADLOCK B EDINGUNGEN 2 DAS P HILOSOPHENPROBLEM ➜ 5 Philosophen speisen mit 5 Gabeln am runden Tisch, wobei jeder zum Essen 2 Gabeln braucht. Ein Philosoph kommt zu einem zufälligen Zeitpunkt an seinen Platz, nimmt zwei Gabeln und ißt zufällig lange. ➜ Eine Lösung des Philosophen Problems besteht darin, dass ein Butler nur maximal vier Philosophen gestattet, am Tisch gleichzeitig Platz zu nehmen. Ein Philosoph muss vor dem Essen erst Platz nehmen, und wenn er fertig ist, steht er auf. DAS P HILOSOPHENPROBLEM 3 B UTLER IN JAVA ➜ Butler entspricht einem Semaphor class Butler { private int value; public Butler(int i) { value=i; } synchronized public void sitdown() { while(value==0) try { wait(); } catch(InterruptedException e) {} --value; } synchronized public void arise() { ++value; notify(); } } B UTLER IN JAVA 4 F ORK . JAVA – M UTEX class Fork { private boolean inUse=false; private int number; private PhilPanel philPanel; public Fork(PhilPanel p, int n) { number=n; philPanel=p; } public synchronized void get() { while(inUse) try { wait(); } catch(InterruptedException e) {} inUse=true; EventQueue.invokeLater(new Runnable() { public void run() { philPanel.drawFork(philPanel.getGraphics(), false, number);}}); } F ORK . JAVA – M UTEX 5 public synchronized void put() { inUse=false; EventQueue.invokeLater(new Runnable() { public void run() { philPanel.drawFork(philPanel.getGraphics(), true, number);}}); notify(); } } F ORK . JAVA – M UTEX 6 P HILT HREAD . JAVA – KONSTANTEN , VARIABLEN , KONSTRUKTOR class PhilThread extends Thread { public static int AWAY=0; public static int HUNGRY=1; public static int GOTRIGHT=2; public static int EATING=3; public static int SITTING=4; private Fork left, right; private Butler butler; private PhilPanel philPanel; private int number; public PhilThread(Fork l, Fork r, Butler b, PhilPanel p, int n) { left=l; right=r; butler=b; philPanel=p; number=n; } ... P HILT HREAD . JAVA – K ONSTANTEN , VARIABLEN , K ONSTRUKTOR 7 P HILT HREAD . JAVA – RUN () ➜ Zwischen allen Aktionen natürlich noch ein entsprechendes sleep() private void draw(int state) { EventQueue.invokeLater(new Runnable() { public void run() { philPanel.drawPhil(philPanel.getGraphics(), state, number);}}); } public void run() { while(true) { draw(AWAY); butler.sitdown(); draw(HUNGRY); right.get(); draw(GOTRIGHT); left.get(); draw(EATING); left.put(); draw(GOTRIGHT); right.put(); draw(SITTING); butler.arise(); P HILT HREAD . JAVA – RUN () } } 8 P HIL PANEL . JAVA – VARIABLEN , PAINT C OMPONENT, KONSTRUKTOR private final static int N=5; private Fork[] forks=new Fork[N]; private PhilThread[] phils=new PhilThread[N]; private int[] philState=new int[N]; private boolean[] forkState=new boolean[N]; private Butler butler; private Image[] imPhil=new Image[5]; public void paintComponent(Graphics g) { super.paintComponent(g); g.drawOval(125, 90, 100, 100); for(int i=0; i<N; i++) { drawPhil(g, philState[i], i); drawFork(g, forkState[i], i); } } P HIL PANEL . JAVA – VARIABLEN , PAINT C OMPONENT, K ONSTRUKTOR 9 public PhilPanel( ) { imPhil[PhilThread.EATING]=Toolkit. getDefaultToolkit().getImage("eating.gif"); imPhil[PhilThread.GOTRIGHT]=Toolkit. getDefaultToolkit().getImage("gotright.gif"); imPhil[PhilThread.HUNGRY]=Toolkit. getDefaultToolkit().getImage("hungry.gif"); imPhil[PhilThread.SITTING]=Toolkit. getDefaultToolkit().getImage("thinking.gif"); butler=new Butler(N-1); for(int i=0; i<N; i++) { forks[i]=new Fork(this, i); forkState[i]=true; } for(int i=0; i<N; i++) { philState[i]=PhilThread.AWAY; phils[i]=new PhilThread(forks[i], forks[((i-1)+N)%N], butler, this, i); phils[i].start( ); } } P HIL PANEL . JAVA – VARIABLEN , PAINT C OMPONENT, K ONSTRUKTOR 10 P HIL PANEL . JAVA – DRAW F ORK public void drawFork(Graphics g,boolean state, int fork) { int x=0, y=0; forkState[fork]=state; switch(fork) { case 0: x=170; y=100; break; case 1: x=200; y=120; break; case 2: x=190; y=160; break; case 3: x=150; y=160; break; case 4: x=140; y=120; break; } if(g!=null) { if(state==true) { g.setColor(Color.black); g.fillOval(x,y,10,10); } else g.clearRect(x,y,10,10); P HIL PANEL . JAVA – DRAW F ORK }} 11 P HIL PANEL . JAVA – DRAW P HIL public void drawPhil(Graphics g, int state, int phil) { int x=0, y=0; philState[phil]=state; switch (phil) { case 0: x=50; y=0; break; case 1: x=200; y=0; break; case 2: x=250; y=100; break; case 3: x=125; y=200; break; case 4: x=0; y=100; break; } if(g!=null) { g.clearRect(x,y,100,90); if(state!=PhilThread.AWAY) g.drawImage(imPhil[state], x, y, null); }} P HIL PANEL . JAVA – DRAW P HIL 12 S YNCHRONISIATION UND P ERFORMANCE ➜ Übermäßige Synchronisation erweist sich oft als Flaschenhals“ ” ➜ Beim Entwurf einer Klasse lässt sich jedoch selten vorhersehen, ob Instanzen von mehreren Threads zugleich genutzt werden ➜ Um auf der sicheren Seite zu sein, sind Modifikatormethoden zustandsbehafteter Objekte, i.a. synchronisiert ➜ Ein gängiger Trick“ Flaschenhälse zu verhindern, ist die ” Verwendung sog. synchronized-wrapper S YNCHRONISIATION UND P ERFORMANCE 13 S YNCHRONIZED -W RAPPERS ➜ Synchronized-Wrappers ermöglichen selektiv unsynchronisierten Zugriff auf Modifikatoren public interface Adder { public void add(int aNumber); } public class UnsyncedAdder implements Adder { int total, numAdditions; public void add(int aNumber) total += aNumber; { numAdditions++; } } public class SyncedAdder implements Adder { Adder adder; public SyncedAdder(Adder a) { adder = a; public synchronized void add(int aNumber) } { adder.add(aNumber); } } S YNCHRONIZED -W RAPPERS 14 S YNCHRONIZED -W RAPPERS IN DEN C OLLECTION API S Beispiel: List myUnsaveList = new ArrayList(CAPACITY); List mySaveList = Collections.synchronizedList(myUnsaveList); // Alternative: Collections.unmodifiableList parallelMutator.execute(mySaveList); Performance-Messungen: JVM 1.2 JVM 1.4 JVM 1.4 -Xint Vector 100% 20% 134% ArrayList 15% 14% 166% Wrapped ArrayList 131% 26% 231% ➜ Quelle: Java Performance Tuning“, Jack Shirazi, O’Reilly 2003 ” S YNCHRONIZED -W RAPPERS IN DEN C OLLECTION API S 15 S YNCHRONISATION GEZIELT VERWENDEN ➜ Beachte: Jede einzelne Akquisition eines Schlosses kann den Anwendungsfluss erheblich drosseln ➜ Beispiel: fetch-emthode einer ungeschickten Cache-Implementierung: public Object fetch(String key) { if(! myHashtable.containsKey(key)) { myHashtable.put(key, createNewEntry(key)); } return myHashtable.get(key); } S YNCHRONISATION GEZIELT VERWENDEN 16 T HREAD -C ONTENTION VERHINDERN ➜ Die Implementierung des Caches, lässt sich optimieren, indem eine unsynchronisierte HashMap verwendet wird ➜ Beachte: In diesem Fall ist die fetch-Methode zu synchronisieren! public synchonized Object fetch(String key) { if(! myHashMap.containsKey(key)) { myHashMap.put(key, createNewEntry(key)); } return myHashMap.get(key); } ➜ Ein Flaschenhals aufgrund ungeschickter Synchronisation wird auch als Thread-Contention Problem bezeichnet ➜ Thread-Contention Probleme lassen sich mit Profilierungs-Tools aufspühren. ➜ siehe z.B.: http://freshmeat.net/projects/contentionprofiler HREAD -C ONTENTION VERHINDERN 17 D EADLOCKFREIE S PERREN ➜ Ein Thread der fortwährend auf ein Schloß wartet, wird als hard-waiting klassifiziert (analog: soft-waiting mit timeout) ➜ Jedes Schloß, das ein Thread in Besitz nimmt wird registriert ➜ Weiterhin speichern wir zu jedem Schloss alle hard-waiting-Threads, so daß ein Wartebaum entsteht ➜ Bevor ein Thread eine neue hard-waiting-Operation auf einem Schloß durchgeführen kann, prüfen wir in einem Tiefendurchlauf, ob das Schloß bereits im Wartebaum dieses Threads enthalten ist und ggf. eine DeadlockDetectedException) ausgelöst ➜ Ein circular-wait ist somit ausgeschlossen Quelle: http://www.onjava.com (O’Reilly online) public class DeadlockDetectedException extends RuntimeException { public DeadlockDetectedException (String s) { super(s); } } D EADLOCKFREIE S PERREN 18 D EADLOCK D ETECTING L OCK zyklische Definition einer Verklemmung (Deadlock): Ein Schloss mit Verklemmung ist ein Schloss, das auf einen Thread mit Verklemmung wartet. Ein Thread mit Verklemmung ist ein Thread, der auf ein Schloss mit Verklemmung wartet. • Technisch gesehen, kann ein Schloss keinen Thread besitzen: aktiv ist nur der Thread, der auf ein Schloss wartet • Wir unterscheiden nicht bei Beziehungen zwischen Schlössern und Threads D EADLOCK D ETECTING L OCK 19 V ERWALTUNG DER S CHL ÖSSER UND T HREADS 2 Listen: Schlösser, die ein Thread hält Alle Schlösser, die ein Thread hält, sind in der DeadlockLockRegistry enthalten DeadlockLockRegistry Threads, die auf ein Schloß warten Zu jedem Schloss gibt es eine Liste aller zugehörigen hardwaitingThreads hardwaitingThreads ➜ Ein Thread besitzt ein Schloss, das wartende Threads hat, die Schlösser haben, auf die andere Threads warten usw. ➜ Durch Beziehungen zwischen Schlössern und Threads wird ein Baum beschrieben: der Wartebaum V ERWALTUNG DER S CHL ÖSSER UND T HREADS 20 S CHLOSSSUCHE IM WAR TEBAUM Prinzip: Deadlocks automatisch verhindern Wartebaum Ein Deadlock tritt auf, wenn ein Thread auf ein Schloss wartet, das in seinem Wartebaum enthalten ist. Bevor ein Thread beginnt auf ein neues Schloss zu warten, prüfen wir, dass dieses Schloss nicht im Wartebaum gespeichert ist (rekursive Tiefensuche) canThreadwaitOnLock ➜ Die die Lock Klassen in Java 5 ermöglichen die Definition von Sperren, bei denen eine Solche Suche implizit durchgeführt wird ➜ ReentrantLock wird erweitert und lock, lockInterruptibly und tryLock werden überschrieben S CHLOSSSUCHE IM WARTEBAUM 21 D EADLOCK D ETECTING L OCK IN JAVA import java.util .*; import java.util.concurrent .*; import java.util.concurrent .locks .*; public class DeadlockDetectingLock extends ReentrantLock { private static List deadlockLocksRegistry = new ArrayList( ); private static synchronized void registerLock(DeadlockDetectingLock ddl) { if (!deadlockLocksRegistry.contains(ddl)) deadlockLocksRegistry.add(ddl); } private static synchronized void unregisterLock(DeadlockDetectingLock ddl) { if ( deadlockLocksRegistry.contains(ddl)) deadlockLocksRegistry.remove(ddl ); } D EADLOCK D ETECTING L OCK IN JAVA 22 D EADLOCK D ETECTING L OCK , CONT. private List hardwaitingThreads = new ArrayList( ); private static synchronized void markAsHardwait(List l, Thread t) { if (!l.contains(t)) l.add(t); } private static synchronized void freeIfHardwait(List l, Thread t) { if (l.contains(t)) l.remove (t); } private static Iterator getAllLocksOwned (Thread t) { DeadlockDetectingLock current; ArrayList results = new ArrayList( ); Iterator itr = deadlockLocksRegistry.iterator( ); while (itr.hasNext( )) { current = (DeadlockDetectingLock)itr.next( ); if (current.getOwner( ) == t) results.add(current); } return results.iterator( ); } private static Iterator getAllThreadsHardwaiting(DeadlockDetectingLock l) { return l.hardwaitingThreads.iterator( ); D EADLOCK D ETECTING L OCK , CONT. } 23 D EADLOCK D ETECTING L OCK , CONT. private static synchronized boolean canThreadWaitOnLock(Thread t, DeadlockDetectingLock l) { Iterator locksOwned = getAllLocksOwned(t); while (locksOwned.hasNext( )) { DeadlockDetectingLock current = (DeadlockDetectingLock)locksOwned.next( ); if (current == l) return false; Iterator waitingThreads = getAllThreadsHardwaiting(current); while (waitingThreads .hasNext( )) { Thread otherthread = (Thread)waitingThreads.next( ); if (!canThreadWaitOnLock(otherthread, l)) { return false; } } return true; } D EADLOCK D ETECTING L OCK , CONT. 24 D EADLOCK D ETECTING L OCK , CONT. public DeadlockDetectingLock ( ) { this(false, false); } public DeadlockDetectingLock (boolean fair) { this(fair , false); } private boolean debugging ; public DeadlockDetectingLock (boolean fair , boolean debug) { super(fair); debugging = debug ; registerLock (this); } public void lock( ) { if (isHeldByCurrentThread( )) { if(debugging) System.out.println("already own lock"); super.lock( ); freeIfHardwait(hardwaitingThreads, Thread.currentThread( )); return; } markAsHardwait(hardwaitingThreads , Thread.currentThread( )); if (canThreadWaitOnLock(Thread.currentThread( ), this)) { if (debugging ) System.out.println("waiting for lock"); super.lock( ); freeIfHardwait(hardwaitingThreads , Thread.currentThread( )); if (debugging) System.out.println ("got new lock"); } else { throw new DeadlockDetectedException("DEADLOCK"); }} D EADLOCK D ETECTING L OCK , CONT. 25 D EADLOCK D ETECTING L OCK , CONT. ➜ Die Methode sumArray hat bei ungünstiger Threadkoordination einen Deadlock zur Folge: public int sumArrays(int [ ] a1, int [ ] a2) { int value = 0; int size = a1.length; if (size == a2.length) { synchronized(a1) { synchronized(a2) { for (int i = 0; i < size; ++i) value += a1 [ i ] + a2 [ i ]; } } } return value; } D EADLOCK D ETECTING L OCK , CONT. 26