280 likes | 380 Views
Muster nebenläufiger Programmierung. concurrent Packet von Java. Concurrent Packet. In diesem Teil der Veranstaltung werde Muster nebenläufiger Programmierung diskutiert. Dazu wird das concurrent Packet von Java betrachtet. Concurrent Packet - Überblick.
E N D
Muster nebenläufiger Programmierung concurrent Packet von Java Alois Schütte AOSD
Concurrent Packet In diesem Teil der Veranstaltung werde Muster nebenläufiger Programmierung diskutiert. Dazu wird das concurrent Packet von Java betrachtet. Alois Schütte AOSD
Concurrent Packet - Überblick Standard-Mittel von Java bezogen auf Nebenläufigkeit sind: Threads mit dem Interface Runnable Synchronisations-Mechanismen synchronized, wait, notify und notifyAll. Ab Java 5 sind im Paket java.util.concurrent Klassen für Standard-Muster der parallelen Programmierung enthalten, z.B.: Locks Queues Thread-Pooling Scheduling Semaphore Exchanger CountDownLatch CyclicBarrier Alois Schütte AOSD
Concurrent Packet - Locks Lockkonzept Ein Lock ist ein Mittel, um in multithreading Umgebungen den gemeinsamen Zugriff auf Ressourcen zu koordinieren. Um eine Ressource nutzen zu können, muss ein Thread den zugehörigen Schlüsselanfordern. Solange ein Thread den Schlüssel besitzt, kann kein anderer Thread die Ressource verwenden, er muss warten. Der den Schlüssel besitzende Thread gibt ihn frei, daraufhin kann ein wartender Thread den Schlüssel bekommen und die Ressource verwenden. Dieses Lockkonzept könnte mit synchronized umgesetzt werden. Dabei hat man aber immer die Blockstruktur als Einschränkung. java.util.concurrent.locks beinhaltet Interfaces und Klassen für Locks. Alois Schütte AOSD
Concurrent Packet - Locks $ cat TimeUnit/MainClass.java importstaticjava.util.concurrent.TimeUnit.*; publicclassMainClassextends Thread { // This fieldis volatile becausetwo different threadsmayaccessit volatile booleankeepRunning = true; publicvoidrun() { while (keepRunning) { longnow = System.currentTimeMillis(); System.out.printf("%tr%n", now); try { Thread.sleep(1000); // millisecs } catch (InterruptedException e) { return; } } } // run In der run-Methode wird die Methode sleep von Thread verwendet. Es wird eine Sekunde geschlafen. Die Klasse TimeUnit wird im Zusammenhang mit Locks verwendet, um eine Zeitdauer in SECONDS, MICROSECONDS, MILLISECONDS oder NANOSECONDS angeben zu können. Alois Schütte AOSD
Concurrent Packet - Locks publicvoidpleaseStop() { keepRunning = false; } publicstaticvoidmain(String[] args) { MainClassthread = newMainClass(); thread.start(); try { SECONDS.sleep(10); // = MILLISECONDS.sleep(10000) } catch (InterruptedExceptionignore) { } thread.pleaseStop(); } // main } $ $ javaMainClass 10:19:51 AM 10:19:52 AM 10:19:53 AM 10:19:54 AM 10:19:55 AM 10:19:56 AM 10:19:57 AM 10:19:58 AM 10:19:59 AM 10:20:00 AM $ • Innerhalb mainwird • ein Thread gestartet, der die Uhrzeit jede Sekunde ausgibt; • SECONDS von TimeUnit verwendet, um das Programm 10 Sekunden lang laufen zu lassen. Alois Schütte AOSD
Concurrent Packet - Locks • Das InterfaceLock spezifiziert das Verhalten von Lock-Objekten. • publicinterface Lock { • void lock(); • voidlockInterruptible() throwsInterruptedException; • booleantryLock(); • booleantryLock(long time, TimeUnitunit) throws • InterruptedException • voidunlock(); • ConditionnewCondition(); // Erklärung später • } • lockwartet, bis der Objektschlüssel verfügbar ist und belegt ihn dann. • unlock gibt das Objekt frei. • lockInterruptible funktioniert wie lock, aber es wird eine Ausnahme geworfen, wenn ein anderer Thread den Thread durch interrupt unterbricht. • tryLock liefert false, wenn der Objektschlüssel nicht verfügbar ist; ansonsten wird derObjektschlüsselin Besitz genommen und true returniert. • tryLock(long, TimeUnit) funktioniert wie tryLock, aber es wird eine maximale Zeitspanne gewartet, wenn das Objekt nicht verfügbar ist. Alois Schütte AOSD
Concurrent Packet – Locks - ReentrantLock Die KlasseReentrantLock implementiert die Schnittstelle Lock. publicclassReentrantLockimplements Lock, Serializable { publicReentrantLock(boolean fair); publicReentrantLock; // Methodsof Lock void lock(); voidlockInterruptible() throwsInterruptedException; booleantryLock(); booleantryLock(long time, TimeUnitunit) throws InterruptedException voidunlock(); ConditionnewCondition(); // additional Methods publicbooleanisFair(); publicintgetHoldCount(); publicintgetQueueLength(); publicbooleanisHeldByCurrentThread(); publicbooleanisLocked(); protected Thread getOwner(); protectedCollection<Thread> getQueuedThreda(); } Alois Schütte AOSD
Concurrent Packet – Locks - ReentrantLock • Der Konstruktor kann die Angabe eine fair-Paramerters haben. Wenn mehrere Threads auf den Lock warten, garantiert fair==true, dass der am längsten wartende Thread das Lock-Objekt erhält. • isFair liefert den fair-Parameter des Konstruktors zurück. • Ein Lock enthält eine Zähler, der bei jedem lock inkrementiert, bei unlock dekrementiert wird. Ein Thread kann also öfter lock aufrufen. getHoldCount liefert den Wert des Zählers. • getQueueLength returniert die Anzahl der auf einen Lock wartenden Threads. • isHeldByCurrentThread ist true, wenn der aufrufende Thread den Lock hält. • isLocked ist true, wenn irgendein Thread den Lock hält. • getOwner(), Collection<Thread> getQueuedThreads liefern den Besitzer und die wartenden Threads. Alois Schütte AOSD
Concurrent Packet – Locks - ReentrantLock $ cat ReentrantLock/synchronized/WithdrawApp.java classAccount { private floatbalance; public Account (floatinitialBalance) { balance = initialBalance; } publicsynchronizedfloatgetBalance() { returnbalance; } // getBalance publicsynchronizedvoidwithdraw( floatamount) { if (amount < 0 || balance < amount) thrownewIllegalArgumentException("withdraw: wrongamount " + amount); try { Thread.sleep(1000); } catch (Exception e) {}; balance -= amount; } // withdraw } // Account synchronized ist erforderlich, da ein Konto von mehreren Threads verwendet werden kann und mindestens einer den Zustand per withdraw ändern kann. Beispiel: Klasse Konto (Account), Geldabholer(Withdrawer als Thread) Zunächst die Realisierung der Klasse Account mit synchronized. Alois Schütte AOSD
Concurrent Packet – Locks - ReentrantLock Nun die Realisierung der Klasse Account mittels Locks. • Die Blockstruktur von synchronized muss mittels lock und unlock nachgebildet werden: importjava.util.concurrent.locks.*; private final ReentrantLock lock = newReentrantLock(true); lock.lock(); try { ... } finally { lock.unlock(); } • Wichtig: • Da im try-Block Ausnahmen auftreten können ist mittels finally sicherzustellen, dass stets unlock aufgerufen wird! • Nur so werden „gelockte“ Objekte immer freigegeben. • Die Verwendung von lock-unlock ist also aufwendiger, dafür aber universell: ein Thread kann lock aufrufen, ein andere unlock • Soll anstelle einer Objektsperre eine Klassensperre deklariert werden, wird die Lock-Variable als static definiert. Alois Schütte AOSD
Concurrent Packet – Locks - ReentrantLock $ cat ReentrantLock/ReentrantLock/WithdrawApp.java importjava.util.concurrent.locks.*; classAccount { private floatbalance; private final ReentrantLock lock = newReentrantLock(true); public Account (floatinitialBalance) { balance = initialBalance; } // Account public float getBalance() { lock.lock(); try { return balance; } finally {lock.unlock();} } // getBalance public void withdraw( float amount) { lock.lock(); try { if (amount < 0 || balance < amount) throw new IllegalArgumentException("withdraw: wrong amount " + amount); try { Thread.sleep(1000); } catch (Exception e) {}; balance -= amount; } finally {lock.unlock();} } // withdraw } // Account Alois Schütte AOSD
Concurrent Packet – Locks - ReentrantLock $ cat ReentrantLock/tryLock/WithdrawApp.java publicvoidwithdraw( floatamount ) { if (lock.tryLock() == false) return; try { if (amount < 0 || balance < amount) throw new IllegalArgumentException("withdraw: ...); try { Thread.sleep(1000); } catch (Exception e) {}; balance -= amount; } finally { lock.unlock(); } } // withdraw Was muss geändert werden, wenn Jenni und Hannah nicht gleichzeitig Geld abholen dürfen? Idee: Der erste Abholer hält den Lock, der zweite muss abgewiesen werden. Lösung: tryLock anstelle von lock Alois Schütte AOSD
Concurrent Packet – Locks - Condition Die Methode newCondition des Interface Lock liefert ein Condition-Objekt zurück. Genauer, ein Objekt einer Klasse die die Schnittstelle Condition implementiert. publicinterfaceCondition { voidawait() thromInterruptedException; voidawaitUninterruptibly(); booleanawait(long time Timeunitunit) thromInterruptedException; longawaitNanos(long time) thromInterruptedException; booleanawaitUntil(Date deadline) thromInterruptedException; voidsignal(); voidsignalAll(); } Die Methoden haben Ähnlichkeit zu wait und notify. Eine Condition ist signalisiert oder nichtsignalisiert. Sofort nach ihrer Erzeugung ist sie signalisiert. Ein await-Aufruf (≈wait) auf einer signalisierten Condition kehrt sofort zurück. Vor Rückkehr von await wird die Condition in den nicht signalisierten Zustand versetzt. signal (≈notify) versetzt eine Condition in den signalisierten Zustand, weckt also einen wartendenThread signalAll (≈notifyAll) weckt alle auf die Condition wartenden Threads. Alois Schütte AOSD
Concurrent Packet – Locks - Condition $ cat Condition/BoundedBuffer/synchronized/BoundedBufferApp.java classBoundedBuffer { private float[] buffer; private intfirst, last; private intnumberInBuffer = 0, size; BoundedBuffer(intlength) { size = length; buffer = newfloat[size]; first = last = 0; } public synchronized void dumpBuffer() { System.err.print("Buffer: "); // use err channel to log for (inti=(first+1)%size, j=0; j<numberInBuffer; j++, i=(i+1)%size) System.err.print(buffer[i] + " "); System.err.println(" "); } prod-ucer con-sumer • float Puffer fester Grösse • dumpBuffer zum Debuggen des Puffers über stderr buffer put get Beispiel: BoundedBuffer, zunächst mit synchronized. Alois Schütte AOSD
Concurrent Packet – Locks - Condition publicsynchronizedvoidput(float item) throwsInterruptedException { while(numberInBuffer == size) wait(); last = (last+1)%size; numberInBuffer++; buffer[last] = item; dumpBuffer(); notifyAll(); } publicsynchronizedfloatget() throwsInterruptedException { while(numberInBuffer == 0) wait(); first = (first+1)%size; numberInBuffer--; dumpBuffer(); notifyAll(); return buffer[first]; } } // BoundedBuffer prod-ucer • Die Methoden put und get sind mittels synchronizedsynchronisiert. • last ist Einfügestelle. • von first wird gelesen. con-sumer buffer put get Beispiel: BoundedBuffer, zunächst mit synchronized. Alois Schütte AOSD
Concurrent Packet – Locks - Condition classProducerextendsThread { private BoundedBufferbuffer; public Producer(BoundedBuffer b) { buffer = b; } publicvoidrun() { for(int i = 0; i < 100; i++) { try { buffer.put(i); System.out.println("put " + i); } catch (InterruptedExceptioningnored) {}; } } } // Producer prod-ucer con-sumer buffer put get Der Produzent verwendet die put-Methode: Alois Schütte AOSD
Concurrent Packet – Locks - Condition $ cat Condition/BoundedBuffer/condition/BoundedBufferApp.java classBoundedBuffer { private float[] buffer; private intfirst, last; private intnumberInBuffer = 0, size; private ReentrantLock lock = newReentrantLock(); private final ConditionnotFull = lock.newCondition(); private final ConditionnotEmpty = lock.newCondition(); BoundedBuffer(intlength) { ... } publicvoiddumpBuffer() { ... } • lock ist ein ReentrantLock Objekt. • Es gibt zwei Condition Attribute, notFull und notEmpty für das Objekt lock. prod-ucer con-sumer buffer put get Wie kann man dies nun mittels Condition realisieren und wo sind die Vorteile? Alois Schütte AOSD
Concurrent Packet – Locks - Condition publicvoidput(float item) throwsInterruptedException { lock.lock(); try { while(numberInBuffer == size) notFull.await(); last = (last+1)%size; numberInBuffer++; buffer[last] = item; dumpBuffer(); notEmpty.signal(); } finally { lock.unlock(); } } • Wenn der Buffer voll ist, wird gewartet, bis eine ConditionnotFull signalisiert wird. • Nach dem Schreiben in den Buffer wird signaliertnotEmpty. prod-ucer con-sumer buffer put get put: Alois Schütte AOSD
Concurrent Packet – Locks - Condition publicfloatget() throwsInterruptedException { lock.lock(); try { while(numberInBuffer == 0) notEmpty.await(); first = (first+1)%size; numberInBuffer--; dumpBuffer(); notFull.signal(); returnbuffer[first]; } finally { lock.unlock(); } } • Wenn der Buffer leer ist, wird gewartet, bis eine ConditionnotEmpty signalisiert wird. • Nach dem Lesen des Buffer wird signaliertnotFull. • Insgesamt ist man also mit Locks und Conditionsflexibler, man kann unterschiedlicheBedingungen signalisieren und so gezielt nur bestimmte Threads wecken (eben die die auf die Condition warten). prod-ucer con-sumer buffer put get get: Alois Schütte AOSD
Concurrent Packet – Executor publicclassHelloWorld { publicstaticvoidmain (String args [ ]) { HelloWorldThread t = newHelloWorldThread ("Hello, World!"); new Thread(t).start(); // creationandstarting a thread } } classHelloWorldThreadimplementsRunnable { // taskof a thread private String str; HelloWorldThread(String s) { str = new String(s); } public void run ( ) { System.out.println(str); } } • In main wird ein Runnable-Objekt t erzeugt (new). Dann muss es explizite gestartet werden. • HelloWorldThreaddefiniert das runnable-Objekt. • In größeren Anwendungen macht es Sinn, strikt zwischen Thread-Management und Anwendungsfunktionalität des Thread zu unterscheiden. • So sollte es auch möglich sein, dass ein Thread mehrere Aufgaben nacheinander ausführt, also die "Threadhülse" mehrfach verwendet werden kann. Bisher gab es stets eine enge Verbindung, zwischen dem was ein Thread macht (definiert im Runnable Objekt) und dem Thread selbst. Alois Schütte AOSD
Concurrent Packet – Executor Objekte, die das Management von Threads übernehmen, werden Executor genannt. Es existieren drei Schnittstellen für Executor: • Executor erlaubt das Erzeugen und Ausführen von Threads • ExecutorService ist ein Subinterface von Executor, das den Lebenszyklus von Thread beeinflussen kann • ScheduledExecutorService erlaubt das Definieren von zukünftigen oder periodischenAbläufen Executorhat eine Methode execute, mit der ein Thread erzeugt und gestartet werden kann. Wenn r ein Runnable Objekt ist und e ein Executor, dann gilt: e.execute(r); == newThread(r)).start(); ExecutorServicebeinhaltet neben execute noch die Methode submit, die ebenfalls ein Runnable-Objekt aufnehmen kann. Die meisten der Executor-Schnittstellen-Implementieirungen benutzen Threadpools. Alois Schütte AOSD
Concurrent Packet – Executor Aufgaben ^ Threadpool erledigte Aufgaben Alternativ müsste für jede Aufgabe immer ein Thread erzeugt werden, dann gelöscht werden, ein neuer Thread müsste erzeugt werden usw. Es existieren unterschiedliche Arten von Threadpools. Hier sei eine stellvertretend behandelt. Bei einem fixedThreadpool gibt es eine feste Menge von Threads. Wenn mehrere Aufgabe zu bearbeiten sind, als Threads verfügbar sind, werden sie in eine Warteschlage eingereiht. Die meisten Implementierungen der Executor-Schnittstellen benutzen Threadpools, die aus Workerthreads bestehen. Die Idee ist es, eine Menge von Workerthreads zu haben, die einmalerzeugt werden und unterschiedlicheAufgaben im Verlauf der Zeit ausführen können. Vorteil: die Threaderzeugung geschieht nur einmal Alois Schütte AOSD
Concurrent Packet – Executor - Threadpools $ catExecutor/ThreadPool/Threadpool.java importjava.util.concurrent.Executors; importjava.util.concurrent.ExecutorService; publicclass Threadpool { publicstaticvoidmain( String[] args ) { Runnabler1 = newRunnable() { publicvoidrun() { System.out.println( "A1 " + Thread.currentThread() ); System.out.println( "A2 " + Thread.currentThread() ); } }; Runnable r2 = new Runnable() { publicvoidrun() { System.out.println( "B1 " + Thread.currentThread() ); System.out.println( "B2 " + Thread.currentThread() ); } }; ^ • r1 und r2 sind zwei Runnable-Objekte, die jeweils eine Aufgabe mit zwei Schritten nacheinander erledigen. • r1 und r2 sollen nebenläufig ablaufen können. Beispiel: 4 Aufgaben mit je zwei Schritten durch einem Threadpool mit 2 Threads. Alois Schütte AOSD
Concurrent Packet – ExecutorExecutor - Threadpools ExecutorService executor = Executors.newFixedThreadPool(2); executor.execute( r1 ); executor.execute( r2 ); try {Thread.sleep(5000);} catch (Exception e) {} System.out.println(); executor.execute( r1 ); executor.execute( r2 ); executor.shutdown(); System.out.println( "Threads started, main ends\n" ); } // end main } // end class Threadpool $ ^ A1 Thread[pool-1-thread-1,5,main] B1 Thread[pool-1-thread-2,5,main] A2 Thread[pool-1-thread-1,5,main] B2 Thread[pool-1-thread-2,5,main] A1 Thread[pool-1-thread-1,5,main] A2 Thread[pool-1-thread-1,5,main] Threads started, mainends B1 Thread[pool-1-thread-2,5,main] B2 Thread[pool-1-thread-2,5,main] • Mittels newFixedThreadPool wird ein Pool mit zwei Threads erzeugt. • executestartet einen Thread aus dem Pool. • shutdownverhindert, dass weitere Workerthreads verwendet werden können. • Ohne shutdown läuft der ExecutorService unendlich (außer System.exit(0);) !!!!!! Name des Pools Name des Thread Priorität des Thread Alois Schütte AOSD
Concurrent Packet – Callable Problem: Ein nebenläufigeThread kann nur über Umwege dem aufrufenden Programm/Thread Ergebnisse mitteilen.Etwa: Runnablerunnable = ...; Thread t = new Thread(runnable); t.start(); t.join(); Stringvalue = someMethodtoGetSavedValue() Lösung: In der Schnittstelle Callable, die Runnable erweitert, lässt sich eine Datenstruktur übergeben, in die der Thread das Ergebnis hineinlegt. Die Datenstruktur kann dann vom Aufrufer auf Änderungen untersucht werden. interfacejava.util.concurrent.Callable<V>{ V call() } Diese Methode enthält den parallel auszuführenden Programmcode und liefert eine Rückgabe vom Typ V. Alois Schütte AOSD
Concurrent Packet – Callable $ cat Callable/CallableApp.java importjava.util.Arrays; importjava.util.concurrent.Executors; importjava.util.concurrent.ExecutorService; importjava.util.concurrent.Callable; importjava.util.concurrent.Future; class Worker implementsCallable<int[]> { private final int[] data; Worker(int[] data) { this.data = data; } publicint[] call() { // overwritecall Arrays.sort(data); returndata; } } // Worker • Worker implementiert Callable. • Callablebietet die Methode call an, die in Worker überschrieben wird.. Beispiel: int-Felder sortieren im Hintergrund durch Callable Alois Schütte AOSD
Concurrent Packet – Callable publicclassCallableApp { publicstaticvoidmain(String[] args) { int[] unsorted = {106,101,110,110,105}; Callable<int[]> c = new Worker(unsorted); ExecutorService executor = Executors.newCachedThreadPool(); Future<int[]> result = executor.submit(c); // worker starts try { int[] sorted = result.get(); // blocks until worker finished for (inti=0; i<sorted.length;i++) System.out.print(sorted[i] + " "); executor.shutdown(); // !!! without shutdown, the executor waits // infinitely } catch (Exception e) {} } // end main } // end class CallableApp • Der ExecutorService bietet eine submit-Methode, die das Callable c annimmt und einen Thread für die Abarbeitung aussucht. • Weil das Ergebnis asynchron ankommt, liefert submit das Future-Objekt result zurück, über das man erkennen kann, ob das Ergebnis schon verfügbar ist. Mitteslresult.get() wird auf das Ergebnis gewartet. • alternativ result.isDone()== trueoder sorted= result.get(2, TimeUnit.SECONDS); um 2 Sekunden zu warten, nach 2 Sekunden wird Ausnahme geworfen. Alois Schütte AOSD