Was ist das Problem? • Safety: Nothing bad ever happens • Liveness: Something good eventually happens • Speed & Responsiveness: Things (seem to) happen faster
Quiescent Consistency In einer "Ruhephase" ist ein Objekt in einem Zustand, der einer beliebigen sequentiellen Ausführung aller vorangegangenen Methoden-Aufrufe entspricht
Locks sind nicht umsonst • Sie bergen das Risiko von Deadlocks • Sie führen zu sequenzialisierter Programmausführung • Sie benötigen zusätzliche Prozessorzyklen ‣ Basieren auf primitiven Operationen (TAS oder CAS), und Speicherbarrieren welche (oft) auf "richtiges" Memory zugreifen müssen
Grundsätze für die Verwendung von Locks Halte genau dann einen Lock, • wenn du auf gemeinsamen und veränder- lichen Zustand zugreifst • wenn du atomare Operationen ausführst ‣ check then act ‣ read-modify-write Halte den Lock nicht länger als nötig
Lock-ordering Deadlock public class SimpleDeadlock... private final Object left = new Object(); private final Object right = new Object(); public void leftRight() { synchronized (left) { synchronized (right) { doSomething(); } } } public void rightLeft() { synchronized (right) { synchronized (left) { doSomething(); } } } lock left try to lock right Thread A: Thread B: lock right wait for ever try to lock left wait for ever
Responsiveness • Besseres Antwortverhalten durch Verlagerung lang-laufender Aufgaben in parallele Threads • Asynchrone Benachrichtigung bei Beendigung / Fortschritt der Berechnung
Wie parallelisiert man ein Programm? • Embarassingly Parallel? • Übliche Strategien ‣ Parallelisierung des Kontrollflusses ‣ Parallellisierung der Daten ‣ Kombination beider Arten
Das Executor-Framework new Thread(...).start() ! <> Executor execute(Runnable) <> ExecutorService submit(Runnable):Future> submit(Callable):Future shutdown() awaitTermination(timeout) <> Future cancel(mayInterrupt) get(): T get(timeout): T <> Callable call(): T Nie wieder
ExecutorService executor = Executors.newCachedThreadPool(); Callable task = new Callable() { @Override public MyType call() { ... //perform long-running task return new MyType(...); } }; Future result = executor.submit(task); ... //do something else result.get(); //wait for end of task Das Executor-Framework Nie wieder new Thread(...).start() !
Atomics public class SynchronizedCounter... private long count = 0; public synchronized long value() { return count; } public synchronized void inc() { count++; } public class AtomicCounter... private AtomicLong count = new AtomicLong(0); public long value() { return count.longValue(); } public void inc() { count.incrementAndGet(); }
public class Shelf { private int capacity; private List products = new ArrayList(); public Shelf(int capacity) { this.capacity = capacity; } public List getProducts() { return products; } public boolean isFull() { return products.size() == capacity; } public void putIn(Product product) { if (isFull()) throw new StorageException("shelf is full."); products.add(product); } public boolean takeOut(Product aBook) { return products.remove(aBook); } }
public class Storehouse { private Map shelves = new HashMap(); public Shelf newShelf(String name, int capacity) { Shelf newShelf = new Shelf(capacity); shelves.put(name, newShelf); return newShelf; } public Shelf getShelf(String name) { return shelves.get(name); } } "No shelf: n28"
public class Shelf... public synchronized int getCapacity() { return capacity; } public synchronized List getProducts() { return products; } public synchronized boolean isEmpty() { return products.isEmpty(); } public synchronized boolean isFull() { return products.size() == capacity; } public synchronized void putIn(Product product) { if (isFull()) throw new StorageException("shelf is full."); products.add(product); } public synchronized boolean takeOut(Product aBook) { return products.remove(aBook); }
public class ValueHolder { private List listeners = new LinkedList(); private int value; public static interface Listener { public void valueChanged(int newValue); } public void addListener(Listener listener) { listeners.add(listener); } public void setValue(int newValue) { value = newValue; for (Listener each : listeners) { each.valueChanged(newValue); } } } The Problem with Threads http://www.eecs.berkeley.edu/Pubs/TechRpts/2006/EECS-2006-1.pdf
Zugrundeliegende Programmiermodell Shared mutable state (aka objects) is accessed in multiple concurrent threads, and we use locks to synchronize / sequentialize access to the state
Das Objekt wird zur "undichten" Abstraktion Um aus einzelnen thread-sicheren Objekten komplexere thread-sichere Objekte zu bauen, muss der Locking-Mechanismus der Einzelobjekte bekannt sein ‣ Verletzt das Prinzip der Kapselung ‣ Verletzt das Prinzip der Modularisierung
Immutability to rescue? • Ohne veränderlichen Zustand, müssen Veränderungen auch nicht synchronisiert werden • Eine verändernde Operation gibt ein neues Objekt zurück
Unveränderliches Groovy-Objekt @Immutable class ImmutableName { String first String last } ImmutableName setLast(newLast) { new ImmutableName(first, newLast) } }
Was passiert mit dem Zustand? • Trick: Wir unterscheiden zwischen Identität eines Objekts und seinem aktuellen Zustand ‣ Zustand repräsentiert durch Immutable Values ‣ Objekt bietet eine sichere Methode an, um Zustand zu aktualisieren • Der überwiegende Teil des Programms arbeitet mit "thread-sicheren" Immutables
@Immutable class Shelf... int capacity List products Shelf putIn(Product product) { if (isFull()) throw new StorageException("shelf is full.") return cloneWith(products: new ArrayList(products) << product) } Shelf takeOut(Product product) { if (!products.contains(product)) return this return cloneWith(products: new ArrayList(products).minus(product)) } private Shelf cloneWith(changes) { def newProps = ... return new Shelf(newProps) }
Immutability im Großen • Performante Implementierung von "Immutable Data Types" ist möglich • In funktionale Programmiersprachen sind unveränderliche Werte die Norm und veränderlicher State die Ausnahme ‣ Beispiel Clojure: Fokus auf unveränderliche Werte und explizite Mechanismen zur Manipulation des aktuellen Werts einer Entity
Transactional Memory • Heap als transaktionale Datenmenge • Transaktionseigenschaften ähnlich wie bei einer Datenbank (ACID) • Optimistische Transaktionen ‣ Transaktionen werden bei einer Kollision automatisch wiederholt • Transaktionen können geschachtelt werden ✘
• Wir können weiterhin in "shared state" und Transaktionen denken • Die korrekte Verwendung ist wesentlich einfacher als bei Locks: Deadlocks sind ausgeschlossen! • Wir gewinnen die Komponierbarkeit von Objekten zurück Vorteile von TM
• Keinerlei Hilfe, wie wir unser sequenzielles Programm parallelisieren. • Der Fortschritt ist nicht garantiert. Livelocks sind möglich. • Ein Programm mit Transaktionen bleibt nicht deterministisch. • Keine Seiteneffekte in Transaktion erlaubt • Die performante und semantisch intuitive Implementierung ist noch Forschungsthema. Nachteile von TM
Fixed Coordination: Parallele Collections • Wir arbeiten auf allen Elementen einer Collection gleichzeitig • Voraussetzung: Die Einzeloperationen sind unabhängig voneinander • Typische parallele Aktionen: ‣ Transformieren ‣ Filtern ‣ Zusammenfassen (reduce)
Explicit Coordination: Message Passing - Actors • Vollständiger Verzicht auf "shared state" • Aktoren empfangen und verschicken Nachrichten ‣ Asynchron und nicht-blockierend ‣ Jeder Actor hat seine "Mailbox" • Immer nur ein aktiver Thread pro Aktor
Actors - Nachteile • Explizite Koordination notwendig! • Kommunikation über asynchrone Nachrichten ist für viele Problemstellungen eine Komplizierung • Nicht geeignet für Probleme, die einen echten Konsens über gemeinsame Objekte erfordern
Aktoren in Erlang • Aktoren: Prozesse + Modul • Selektiver Nachrichtenempfang • "Warten auf Nachrichten" als Continuation • Endrekursive Funktionen zur Zustands
Erlang Process Patterns • Process should encapsulate an activity, not a task • Archetypes of processes ‣ Client / Server ‣ Finite State Machine ‣ Event Manager / Event Handler • Supervisor hierarchies for robustness and fault tolerance
JVM-Probleme für nebenläufiges Programmieren • Kurz- und mittelfristig ‣ keine Closures ‣ keine nativen persistenten Datenstrukturen ‣ die meisten Java-Bibliotheken nicht thread-sicher • Langfristig ‣ Keine Unterstützung von Coroutines/Continuations ‣ Late binding verhindert Sicherheit ‣ Task-basierte Concurrency skaliert nicht gut ‣ Keine performante Kommunikation zwischen Tasks ‣ Keine Optimierung für End-Rekursion
Agents • Sie kapseln nicht threadsichere (Java-) Komponenten • Konzeptionell ähnlich wie Agents in Clojure • Implementierung unterscheiden sich stark in Effizienz
Dataflow-Operators final leftAddend = new DataflowQueue() final rightAddend = new DataflowQueue() final sum = new DataflowQueue() operator(inputs: [leftAddend, rightAddend], outputs: [sum], { left, right -> sum << left + right }) task { [10, 20, 30].each { leftAddend << it } } task { [100, 200, 300].each { rightAddend << it } } [110, 220, 330].each { assert it == sum.val }
Vorteile von Dataflows • Kein Locking notwendig • Keine Race-Conditions • Deterministisch ‣ Deadlocks sind möglich, sie passieren dann aber immer! • Kein Unterschied zwischen sequenziellem und nebenläufigem Code • Skalieren gut
Mehr über Data Flows • Einschränkungen ‣ Nicht für jede Problemstellung geeignet ‣ Berechnungen dürfen keine Seiteneffekte haben • Erweiterungen ‣ Data flow streams, Data flow operators • Andere JVM-Implementierungen ‣ Scala Dataflow (akka), FlowJava (akademisch & tot)
Takeaways • Shared mutable state ist böse • Andere Paradigmen sind einfacher zu verstehen, erfordern aber häufig ein Umdenken bei Design / Architektur • Java als Sprache hat zu viel Zeremonie, um alternative Ansätze knapp und lesbar schreiben zu können ‣ Groovy / Gpars sehr gut zum Experimentieren • JVM als Plattform ist für manche Ansätze nicht optimal geeignet