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
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
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
submit(Runnable):Future<?> submit(Callable<T>):Future<T> shutdown() awaitTermination(timeout) <<Interface>> Future<T> cancel(mayInterrupt) get(): T get(timeout): T <<Interface>> Callable<T> call(): T Nie wieder
@Override public MyType call() { ... //perform long-running task return new MyType(...); } }; Future<MyType> result = executor.submit(task); ... //do something else result.get(); //wait for end of task Das Executor-Framework Nie wieder new Thread(...).start() !
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(); }
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
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
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
{ 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) }
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
wie bei einer Datenbank (ACID) • Optimistische Transaktionen ‣ Transaktionen werden bei einer Kollision automatisch wiederholt • Transaktionen können geschachtelt werden ✘
• Die korrekte Verwendung ist wesentlich einfacher als bei Locks: Deadlocks sind ausgeschlossen! • Wir gewinnen die Komponierbarkeit von Objekten zurück Vorteile von TM
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
"shared state" • Aktoren empfangen und verschicken Nachrichten ‣ Asynchron und nicht-blockierend ‣ Jeder Actor hat seine "Mailbox" • Immer nur ein aktiver Thread pro Aktor
asynchrone Nachrichten ist für viele Problemstellungen eine Komplizierung • Nicht geeignet für Probleme, die einen echten Konsens über gemeinsame Objekte erfordern
a task • Archetypes of processes ‣ Client / Server ‣ Finite State Machine ‣ Event Manager / Event Handler • Supervisor hierarchies for robustness and fault tolerance
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
• Deterministisch ‣ Deadlocks sind möglich, sie passieren dann aber immer! • Kein Unterschied zwischen sequenziellem und nebenläufigem Code • Skalieren gut
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