10. Angenehme Position des
Halbwissens
"Nebenläufigkeit wird
völlig überbewertet"
"Das erledigen die
Frameworks für mich"
11. Angenehme Position des
Halbwissens
"Nebenläufigkeit wird
völlig überbewertet"
"Das erledigen die
Frameworks für mich"
"Ich trenne meine Fachlogik von
Nebenläufigkeit und Threading"
12. Angenehme Position des
Halbwissens
"Nebenläufigkeit wird
völlig überbewertet"
"Das erledigen die
Frameworks für mich"
"Ich trenne meine Fachlogik von
Nebenläufigkeit und Threading"
"Ich denke mal ganz intensiv
darüber nach und dann
funktioniert das"
38. Locks sind nicht umsonst
• Sie bergen das Risiko von Deadlocks
• Sie führen zu sequenzialisierter
Programmausführung
39. 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), welche (oft) auf "richtiges"
Memory zugreifen müssen
49. Sicheres Publizieren -
Safe Publication
• Initialisierung in statischem Initializer
• Speichern in volatile-Feld oder in
AtomicReference
• Ein unveränderliches Objekt
• Speichern in einem Feld, das vollständig
durch einen Lock abgesichert ist
51. Grundsätze für die
Verwendung von Locks
Halte genau dann einen Lock,
• wenn du auf gemeinsamen und veränder-
lichen Zustand zugreifst
52. 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
53. 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
54. Liveness - Jeder sollte an
die Reihe kommen
• Gefahr 1: Deadlocks
• Gefahr 2: Starvation
55. Deadlock
public class SimpleDeadlock...
private final Object left Thread A: Thread B:
= new Object();
private final Object right lock left
= new Object();
public void leftRight() { lock right
synchronized (left) {
synchronized (right) { try to
doSomething(); lock right
} try to
} lock left
}
public void rightLeft() {
synchronized (right) {
synchronized (left) { wait for ever
doSomething();
} wait for ever
}
}
56. Starvation
Bei hohem "Wettbewerb" (contention) um
die gleiche Ressource (z.B. Lock)
kommen nicht alle wartenden Threads an
die Reihe
57. Performance
• Mehr Prozessoren/Kerne sollten zu
besserer Performance führen
‣ Besseres Antwortverhalten durch
Verlagerung lang-laufender Berechnungen
in parallele Threads
‣ Höherer Durchsatz durch Aufteilung von
Berechnungen in mehrere Threads
72. public class MyNewClass...
private String myProp = "";
private Lock lock = new ReentrantLock();
private Condition propNotEmpty = lock.newCondition();
public String getMyProp() {
lock.lock();
try {
return myProp;
} finally {lock.unlock();}
}
public void setMyProp(String value) {
lock.lock();
try {
myProp = value;
propNotEmpty.signalAll();
} finally {lock.unlock();}
}
public void waitUntilPropNotEmpty() throws InterruptedException {
lock.lock();
try {
while (myProp.equals("")) {
propNotEmpty.await();
}
} finally {lock.unlock();}
}
73. public class MyNewClass...
private String myProp = "";
private Lock lock = new ReentrantLock();
private Condition propNotEmpty = lock.newCondition();
public String getMyProp() {
lock.lock();
try {
return myProp;
} finally {lock.unlock();}
}
public void setMyProp(String value) {
lock.lock();
try {
myProp = value;
propNotEmpty.signalAll();
} finally {lock.unlock();}
}
public void waitUntilPropNotEmpty() throws InterruptedException {
lock.lock();
try {
while (myProp.equals("")) {
propNotEmpty.await();
}
} finally {lock.unlock();}
}
74. public class MyNewClass...
private String myProp = "";
private Lock lock = new ReentrantLock();
private Condition propNotEmpty = lock.newCondition();
public String getMyProp() {
lock.lock();
try {
return myProp;
} finally {lock.unlock();}
}
public void setMyProp(String value) {
lock.lock();
try {
myProp = value;
propNotEmpty.signalAll();
} finally {lock.unlock();}
}
public void waitUntilPropNotEmpty() throws InterruptedException {
lock.lock();
try {
while (myProp.equals("")) {
propNotEmpty.await();
}
} finally {lock.unlock();}
}
75. Implizite oder explizite
Locks / Conditions
• Verwende implizite Locks und Conditions,
wenn dir deren Feature-Set genügt
‣ Performance seit Java 6 praktisch gleich
• Verwende explizite Locks und Conditions,
wenn du mehr brauchst
‣ Unterbrechbarkeit
‣ tryLock()
‣ Timeouts
‣ Mehr als eine Condition-Queue
76. Das Executor-Framework
Nie wieder new Thread(...).start() !
<<Interface>>
Executor
<<Interface>>
execute(Runnable) Callable<T>
call(): T
<<Interface>>
ExecutorService
<<Interface>>
submit(Runnable):Future<?>
Future<T>
submit(Callable<T>):Future<T>
shutdown() cancel(mayInterrupt)
awaitTermination(timeout) get(): T
get(timeout): T
77. public class MyNewClassTest...
private volatile boolean succeeded = false;
@Test
public void waitingForAValue() throws Exception {
final MyNewClass my = new MyNewClass();
ExecutorService executor = Executors.newCachedThreadPool();
Runnable task = new Runnable() {
@Override
public void run() {
try {
my.waitUntilPropNotEmpty();
succeeded = true;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
};
Future<?> result = executor.submit(task);
my.setMyProp("");
assertFalse(succeeded);
my.setMyProp("test");
result.get(); //wait for end of task
assertTrue(succeeded);
}
78. public class MyNewClassTest...
private volatile boolean succeeded = false;
@Test
public void waitingForAValue() throws Exception {
final MyNewClass my = new MyNewClass();
ExecutorService executor = Executors.newCachedThreadPool();
Runnable task = new Runnable() {
@Override
public void run() {
try {
my.waitUntilPropNotEmpty();
succeeded = true;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
};
Future<?> result = executor.submit(task);
my.setMyProp("");
assertFalse(succeeded);
my.setMyProp("test");
result.get(); //wait for end of task
assertTrue(succeeded);
}
79. public class MyNewClassTest...
private volatile boolean succeeded = false;
@Test
public void waitingForAValue() throws Exception {
final MyNewClass my = new MyNewClass();
ExecutorService executor = Executors.newCachedThreadPool();
Runnable task = new Runnable() {
@Override
public void run() {
try {
my.waitUntilPropNotEmpty();
succeeded = true;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
};
Future<?> result = executor.submit(task);
my.setMyProp("");
assertFalse(succeeded);
my.setMyProp("test");
result.get(); //wait for end of task
assertTrue(succeeded);
}
81. 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();
}
82. Compare and Set
public class HandmadeAtomicReference<V>...
private V ref;
public HandmadeAtomicReference(V initialValue) {...}
public synchronized V get() { return ref; }
/**
* Atomically sets the value to the given updated value
* if the current value {@code ==} the expected value.
* @param expect the expected value
* @param update the new value
* @return true if successful.
*/
public synchronized boolean compareAndSet(V expect, V update) {
if (ref != expect)
return false;
ref = update;
return true;
}
83. Und noch mehr...
• Concurrent Collections
• BlockingQueue
• Latches, Barriers, Exchangers & Co
85. Mein Lieblingsbeispiel:
Banking
Bank
createNewAccountFor(customer): Account
accountByNumber(long accNum): Account
allAccountNumbers(): Collection<long>
uses
Account
MoneyTransfer
accountNumber: long
customer: String amount: BigDecimal
balance: BigDecimal from, to: long
deposit(amount) execute(Bank bank)
withdraw(amount)
86. public class Account {
private BigDecimal balance = BigDecimal.ZERO;
private final long accountNumber;
private final String customer;
public Account(long accountNumber, String customer) {...}
public void deposit(BigDecimal amount) {
balance = balance.add(amount);
}
public void withdraw(BigDecimal amount) {
balance = balance.subtract(amount);
}
public BigDecimal getBalance() {
return balance;
}
public long getAccountNumber() {
return accountNumber;
}
public String getCustomer() {
return customer;
}
}
87. public class Bank {
private Map<Long, Account> accounts = new HashMap<Long, Account>();
private long lastAccountNumber = 0;
public Account createNewAccountForString(String customer) {
Account account = new Account(++lastAccountNumber, customer);
accounts.put(account.getAccountNumber(), account);
return account;
}
public Account accountByNumber(long accountNumber) {
return accounts.get(accountNumber);
}
}
88. public class AccountCreationTest extends ParallelTestCase...
private volatile Bank bank = new Bank();
@Test
public void createAccountsInManyThreads() throws Exception {
final int numberOfAccounts = 100;
final AtomicInteger account = new AtomicInteger(0);
Runnable createAccountTask = new Runnable() {
@Override
public void run() {
String customer = "c" + account.incrementAndGet();
long accountNumber = bank.createNewAccountForString(
customer).getAccountNumber();
assertEquals(customer, bank.accountByNumber(
accountNumber).getCustomer());
}
};
runInParallelThreads(numberOfAccounts, createAccountTask);
assertAllAccountsHaveBeenCreated(numberOfAccounts);
}
89. "No account with number 99"
public class Bank {
private Map<Long, Account> accounts = new HashMap<Long, Account>();
private long lastAccountNumber = 0;
public Account createNewAccountForString(String customer) {
Account account = new Account(++lastAccountNumber, customer);
accounts.put(account.getAccountNumber(), account);
return account;
}
public Account accountByNumber(long accountNumber) {
return accounts.get(accountNumber);
}
}
90. public class Bank {
private Map<Long, Account> accounts = new HashMap<Long, Account>();
private AtomicLong lastAccountNumber = new AtomicLong(0L);
public Account createNewAccountForString(String customer) {
Account account =
new Account(lastAccountNumber.incrementAndGet(), customer);
accounts.put(account.getAccountNumber(), account);
return account;
}
public Account accountByNumber(long accountNumber) {
return accounts.get(accountNumber);
}
}
91. NullPointerException
public class Bank {
private Map<Long, Account> accounts = new HashMap<Long, Account>();
private AtomicLong lastAccountNumber = new AtomicLong(0L);
public Account createNewAccountForString(String customer) {
Account account =
new Account(lastAccountNumber.incrementAndGet(), customer);
accounts.put(account.getAccountNumber(), account);
return account;
}
public Account accountByNumber(long accountNumber) {
return accounts.get(accountNumber);
}
}
92. public class Bank {
private final Map<Long, Account> accounts =
new ConcurrentHashMap<Long, Account>();
private final AtomicLong lastAccountNumber = new AtomicLong(0L);
public Account createNewAccountForString(String customer) {
Account account =
new Account(lastAccountNumber.incrementAndGet(), customer);
accounts.put(account.getAccountNumber(), account);
return account;
}
public Account accountByNumber(long accountNumber) {
return accounts.get(accountNumber);
}
}
93. ok
public class Bank {
private final Map<Long, Account> accounts =
new ConcurrentHashMap<Long, Account>();
private final AtomicLong lastAccountNumber = new AtomicLong(0L);
public Account createNewAccountForString(String customer) {
Account account =
new Account(lastAccountNumber.incrementAndGet(), customer);
accounts.put(account.getAccountNumber(), account);
return account;
}
public Account accountByNumber(long accountNumber) {
return accounts.get(accountNumber);
}
}
94. public class Account...
private final AtomicReference<BigDecimal> balance =
new AtomicReference<BigDecimal>(BigDecimal.ZERO);
public void deposit(BigDecimal amount) {
while (true) {
BigDecimal oldAmount = balance.get();
BigDecimal newAmount = oldAmount.add(amount);
if (balance.compareAndSet(oldAmount, newAmount))
return;
}
}
public void withdraw(BigDecimal amount) {
while (true) {
BigDecimal oldAmount = balance.get();
BigDecimal newAmount = oldAmount.subtract(amount);
if (balance.compareAndSet(oldAmount, newAmount))
return;
}
}
public BigDecimal getBalance() {
return balance.get();
}
95. public class Account...
private final AtomicReference<BigDecimal> balance =
new AtomicReference<BigDecimal>(BigDecimal.ZERO);
public void deposit(BigDecimal amount) {
while (true) {
BigDecimal oldAmount = balance.get();
BigDecimal newAmount = oldAmount.add(amount);
if (balance.compareAndSet(oldAmount, newAmount))
return;
}
}
public void withdraw(BigDecimal amount) {
while (true) {
BigDecimal oldAmount = balance.get();
BigDecimal newAmount = oldAmount.subtract(amount);
if (balance.compareAndSet(oldAmount, newAmount))
return;
}
}
public BigDecimal getBalance() {
return balance.get();
}
96. public class Account...
private BigDecimal balance = BigDecimal.ZERO;
public synchronized void deposit(BigDecimal amount) {
balance = balance.add(amount);
}
public synchronized void withdraw(BigDecimal amount) {
balance = balance.subtract(amount);
}
public synchronized BigDecimal getBalance() {
return balance;
}
97. public class MoneyTransfer {
private final BigDecimal amount;
private final long from;
private final long to;
public MoneyTransfer(BigDecimal amount, long from, long to) {
this.amount = amount;
this.from = from;
this.to = to;
}
public void execute(Bank bank) {
Account source = bank.accountByNumber(from);
Account target = bank.accountByNumber(to);
if (source.getBalance().compareTo(amount) < 0)
throw new InsufficientFundsException();
source.withdraw(amount);
target.deposit(amount);
}
}
98. public class MoneyTransfer {
private final BigDecimal amount;
private final long from;
private final long to;
public MoneyTransfer(BigDecimal amount, long from, long to) {
this.amount = amount;
this.from = from;
this.to = to;
}
synchronized bank) {
public void execute(Bank void execute(Bank bank) {
(
Account source = bank.accountByNumber(from);
Account target = bank.accountByNumber(to);
if (source.getBalance().compareTo(amount) < 0)
throw new InsufficientFundsException();
source.withdraw(amount);
target.deposit(amount);
}
}
99. public class MoneyTransfer...
public void execute(Bank bank) {
Account source = bank.accountByNumber(from);
Account target = bank.accountByNumber(to);
synchronized (source) {
synchronized (target) {
doTransfer(source, target);
}
}
}
private void doTransfer(Account source, Account target) {
if (source.getBalance().compareTo(amount) < 0)
throw new InsufficientFundsException();
source.withdraw(amount);
target.deposit(amount);
}
100. public class MoneyTransfer...
public void execute(Bank bank) {
Account source = bank.accountByNumber(from);
Account target = bank.accountByNumber(to);
synchronized (source) {
synchronized (target) {
doTransfer(source, target);
(
}
}
}
private void doTransfer(Account source, Account target) {
if (source.getBalance().compareTo(amount) < 0)
throw new InsufficientFundsException();
source.withdraw(amount);
target.deposit(amount);
}
// Thread A:
new MoneyTransfer(new BigDecimal(2), 234, 789).execute(bank);
// Thread B:
new MoneyTransfer(new BigDecimal(2), 789, 234).execute(bank);
101. public class MoneyTransfer...
public void execute(Bank bank) {
Account source = bank.accountByNumber(from);
Account target = bank.accountByNumber(to);
Object[] locks = new Object[] { source, target };
Arrays.sort(locks);
synchronized (locks[0]) {
synchronized (locks[1]) {
doTransfer(source, target);
}
}
}
public class Account implements Comparable<Account>...
public int compareTo(Account other) {
return new Long(accountNumber).
compareTo(new Long(other.accountNumber));
}
102. Für den normal sterblichen
Entwickler ist die Erstellung
korrekter Multi-Thread-
Programme außerhalb ihrer
Fähigkeiten
103. public class ValueHolder {
private List<Listener> listeners = new LinkedList<Listener>();
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
104. Zugrundeliegende
Programmiermodell
Shared mutable state (aka objects)
is accessed in multiple concurrent threads,
and we use locks to synchronize /
sequentialize access to the state
105. 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 Prinzip der Kapselung
‣ Verletzt Prinzip der Modularisierung
108. Transactional Memory
(TM)
• Heap als transaktionale Datenmenge
• Transaktionseigenschaften ähnlich wie
bei einer Datenbank (ACID)
• Optimistische Transaktionen
• Transaktionen werden bei einer
Kollision automatisch wiederholt
• Transaktionen können geschachtelt und
komponiert werden
109. public class Account...
public void deposit(BigDecimal amount) {
atomic {
balance = balance.add(amount);
}
}
public void withdraw(BigDecimal amount) {
atomic {
balance = balance.subtract(amount);
}
}
110. public class Account...
public void deposit(BigDecimal amount) {
atomic {
balance = balance.add(amount);
}
}
public void withdraw(BigDecimal amount) {
atomic {
balance = balance.subtract(amount);
}
}
111. public class Account...
public void deposit(BigDecimal amount) {
atomic {
balance = balance.add(amount);
}
}
public void withdraw(BigDecimal amount) {
atomic {
balance = balance.subtract(amount);
}
}
public class MoneyTransfer...
public void execute(Bank bank) {
atomic {
Account source = bank.accountByNumber(from);
Account target = bank.accountByNumber(to);
if (source.getBalance().compareTo(amount) < 0)
throw new InsufficientFundsException();
source.withdraw(amount);
target.deposit(amount);
}
}
112. Eigenschaften von TM
• Vorteile:
‣ Wir können weiter in shared state und
Transaktionen denken
‣ Die korrekte Verwendung ist wesentlich
einfacher als bei Locks: Deadlocks sind
ausgeschlossen!
• Nachteile:
‣ Keinerlei Hilfe, wie wir unser sequenzielles
Programm parallelisieren
‣ Der Fortschritt ist nicht garantiert. Livelocks
sind möglich
‣ Die performante Implementierung ist noch
Forschungsthema
113. TM-Implementierungen
• Hardware Transactional Memory
• Software Transactional Memory
‣ Clojure: Programmiersprache auf der JVM,
die STM eingebaut hat
‣ Java-STM-Frameworks:
Deuce, Akka, Multiverse
116. Immutability
• Ohne veränderlichen Zustand, müssen
Veränderungen auch nicht
synchronisiert werden
• Systeme haben aber doch einen Zustand
der sich ändert, oder?
117. Immutability
• Ohne veränderlichen Zustand, müssen
Veränderungen auch nicht
synchronisiert werden
• Systeme haben aber doch einen Zustand
der sich ändert, oder?
• Trick: Wir unterscheiden zwischen
unveränderlichen Werten (values) und
einer bestimmten Entity zugeordnenten
aktuellen Wert
121. @Immutable
class MoneyTransfer {
BigDecimal amount
long from, to
boolean execute(Bank bank) {
while(true) {
Account source = bank.accountByNumber(from);
Account target = bank.accountByNumber(to);
if (source.balance < amount)
return false
source = source.withdraw(amount)
target = target.deposit(amount)
if (bank.update(source, target))
return true
}
}
}
122. 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
123. 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
124. class BankActor extends AbstractPooledActor...
void act() {
loop {
react { message ->
switch(message) {
case CreateAccount:
createNewAccountFor(message.customer); break
case GetAccount:
accountByNumber(message.accountNumber); break
case GetAllAccountNumbers:
allAccountNumbers(); break
case UpdateAccounts:
update(message.accounts); break
default:
println "${this.class}: I don't understand $message"
} } } }
@Immutable class CreateAccount { String customer }
@Immutable class UpdateAccounts { Account[] accounts }
@Immutable class GetAccount { long accountNumber }
@Immutable class GetAllAccountNumbers {}
128. Fehler-tolerant &
hoch-verfügbar
• Mehrere redundante Aktoren stehen für
die gleiche Aufgabe zur Verfügung
• Organisation in Supervisor-Hierarchien
‣ Kein lokales Exception-Handling, sondern
Neustart eines Aktors im Fehlerfalle
129. Actors - Nachteile
• Nicht geeignet für Probleme, die einen
echten Konsens über gemeinsame
Objekte erfordern
• Kommunikation über asynchrone
Nachrichten ist für viele
Problemstellungen eine Komplizierung
130. Parallele Collections
• Wir arbeiten auf allen Elementen einer
Collection gleichzeitig
• Voraussetzung: Die Einzeloperationen
sind unabhängig voneinander
• Typische parallele Aktionen:
‣ Filtern
‣ Transformieren
131. Beispiel:
Standardabweichung aller
Millionärskonten von
1.000.000
transform filter transform
Kontonummer Saldo Mio-Saldo Diff2Mio
transform filter transform
Kontonummer transform Saldo filter Mio-Saldo transform Diff2Mio
Kontonummer transform Saldo filter Mio-Saldo transform Diff2Mio
Kontonummer transform Saldo filter Mio-Saldo transform Diff2Mio
transform Saldo filter Mio-Saldo transform Diff2Mio
sum
Kontonummer
Kontonummer transform Saldo filter Mio-Saldo transform Diff2Mio
Kontonummer transform Saldo filter Mio-Saldo transform Diff2Mio
Kontonummer transform Saldo filter Mio-Saldo transform Diff2Mio
mier
Kontonummer Saldo Mio-Saldo Diff2Mio
su
m
en
m
ier
en
Std-Dev
136. MapReduce
• Von Google patentierter Ansatz zur
verteilten Bearbeitung großer
Datencluster
• Name stammt von den map- und reduce-
Funktionen funktionaler Sprachen
• Idee: Einzelne Datensätze werden durch
eine Pipeline von Arbeitsschritten
transformiert (map) und am Ende zu
einem Ergebnis zusammengeführt
(reduce)
138. Data Set map Data Set
Data Set map Data Set
Data Set map Data Set
Data Set map Data Set
Data Set map Data Set
Data Set map Data Set
139. Data Set map Data Set
reduce
Data Set map Data Set
Data Set map Data Set reduce
reduce
Data Set map Data Set
reduce
Data Set map Data Set
Result
reduce
Data Set map Data Set
140. Data Set map Data Set
reduce
Data Set map Data Set
Data Set map Data Set reduce
reduce
Data Set map Data Set
reduce
Data Set map Data Set
Result
reduce
Data Set map Data Set
141. Map-Reduce mit gpars
Parallelizer.doParallel(numThreads) {
def deviations = bank.allAccountNumbers().parallel.map {accountNumber ->
bank.accountByNumber(accountNumber).balance
}.filter { balance -> balance >= MILLION }.map {balance ->
def diffToMean = balance - MILLION
(diffToMean * diffToMean)
}
//def sum = deviations.sum()
def sum = deviations.reduce {a,b -> a + b}
def count = deviations.size()
def average = Math.sqrt(sum / count)
}
142. Kritik an MapReduce
• Patentierbarkeit (claim of novelty)
wird angezweifelt
• Wie breit ist der Problemraum, für den
MapReduce sinnvoll angewandt werden
kann?
• Wie sinnvoll ist MapReduce im nicht
verteilten Kontext?
150. Data Flows
• Beschreibung der Abhängigkeiten von
Daten untereinander
• Grundkonzept: Dataflow-Variablen
‣ Wert kann nur einmal gebunden werden
‣ Abfrage des Wertes blockiert bis er
gebunden wurde
151. gpars - DataFlows
def x = new DataFlowVariable()
def y = new DataFlowVariable()
def z = new DataFlowVariable()
task { z << x.val + y.val }
task { x << 40 }
task { y << 2 }
assert 42 == z.val
152. gpars - DataFlows
def x = new DataFlowVariable()
def y = new DataFlowVariable()
def z = new DataFlowVariable()
task { z << x.val + y.val }
task { x << 40 }
task { y << 2 }
assert 42 == z.val
def df = new DataFlows()
task { df.z = df.x + df.y }
task { df.x = 40 }
task { df.y = 2 }
assert 42 == df.z
153. 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
155. 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
• JVM-Implementierungen
‣ Scala Dataflow, FlowJava (akademisch & tot)
156. Nicht behandelt
• Fork/Join (jsr166y)
• CSP:
Communicating Sequential Processes
• Declarative Concurrency
• und andere...
157. Fazit
• Multi-thread-orientierte und Lock-
basierte Programmierung ist schwierig
‣ Hauptgrund: Thread-sichere Objekte sind
zusammengesetzt nicht komponierbar
• "Immutability" erleichtert das parallele
Leben sehr
• Andere Paradigmen können helfen, der
heilige Kral der Multi-Core-Entwicklung
ist aber (noch) nicht gefunden
158. Links
Zum Lesen
http://www.infoq.com/presentations/
goetz-concurrency-past-present
http://www.slideshare.net/jboner/
state-youre-doing-it-wrong-javaone-2009
http://ajaxonomy.com/2008/news/
toward-better-concurrency
http://igoro.com/archive/
gallery-of-processor-cache-effects/
Zum Ausprobieren
http://gpars.codehaus.org/
http://clojure.org/
http://github.com/jboner/scala-dataflow
Hinweis der Redaktion
Meine pers&#xF6;nliche Geschichte beginnt im Jahre 2000.
Es war das Jahr, in dem ich mich das erste mal ein intensiver mit Javas Multi-Threading-F&#xE4;higkeiten auseinandergesetzt habe. Zuf&#xE4;llig war das im Rahmen einer der ersten von mir mit TDD entwickelten Programmen, eine Logging-Framework.
Diese Klasse enth&#xE4;lt fast alles, was man fr&#xFC;her (vor JDK 1.5) &#xFC;ber Nebenl&#xE4;ufigkeit und Synchronisation in Java wissen musste.
Das volatile f&#xFC;r das myProp-Member ist &#xFC;brigens &#xFC;berfl&#xFC;ssig.
Hier sieht man, wie man eigene Threads erzeugt, startet und auf ihr Ende wartet (join).
Umfrage: Wer kennt die hier verwendeten Java-Konstrukte und wer nicht?
Die Sprachkonstrukte waren also vergleichsweise wenige, beim Einsatz war ich schon immer sehr sparsam und habe mich auf die Position zur&#xFC;ckgezogen:
In der Regel brauche ich keine Nebenl&#xE4;ufigkeit und wenn, dann erledigt das entweder ein Framework f&#xFC;r mich (z.B. Web-Container verteilt Requests an Prozesse) oder - wenn sich Nebel&#xE4;ufigkeit und Threads nicht vermeiden lassen, dann konzentriere ich mich darauf, Fachlogik von Synchronisation und Threading schon im Objektmodell eindeutig zu trennen, und dann denke ich mal ganz dolle &#xFC;ber die wenigen nebenl&#xE4;ufigen Klassen nach.
Bis vor wenigen Jahren bin ich mit dieser Strategie auch gut gefahren, denn die Gelegenheiten, in den Nebenl&#xE4;ufigkeit bei meinen Programmier- und Beratungsjobs zum Thema wurden, waren wenige, doch seit etwa 3 Jahren stolpere ich immer h&#xE4;ufiger &#xFC;ber Artikel, Vortr&#xE4;ge und Frameworks zu diesem Thema. Ja, ganze Programmiersprachen und -Paradigmen (funktionale Programmierung) prahlen damit, dass sie sich perfekt f&#xFC;r nebenl&#xE4;ufige Prgrammierung eignen. Warum die Renaissance des Themas?
Die Sprachkonstrukte waren also vergleichsweise wenige, beim Einsatz war ich schon immer sehr sparsam und habe mich auf die Position zur&#xFC;ckgezogen:
In der Regel brauche ich keine Nebenl&#xE4;ufigkeit und wenn, dann erledigt das entweder ein Framework f&#xFC;r mich (z.B. Web-Container verteilt Requests an Prozesse) oder - wenn sich Nebel&#xE4;ufigkeit und Threads nicht vermeiden lassen, dann konzentriere ich mich darauf, Fachlogik von Synchronisation und Threading schon im Objektmodell eindeutig zu trennen, und dann denke ich mal ganz dolle &#xFC;ber die wenigen nebenl&#xE4;ufigen Klassen nach.
Bis vor wenigen Jahren bin ich mit dieser Strategie auch gut gefahren, denn die Gelegenheiten, in den Nebenl&#xE4;ufigkeit bei meinen Programmier- und Beratungsjobs zum Thema wurden, waren wenige, doch seit etwa 3 Jahren stolpere ich immer h&#xE4;ufiger &#xFC;ber Artikel, Vortr&#xE4;ge und Frameworks zu diesem Thema. Ja, ganze Programmiersprachen und -Paradigmen (funktionale Programmierung) prahlen damit, dass sie sich perfekt f&#xFC;r nebenl&#xE4;ufige Prgrammierung eignen. Warum die Renaissance des Themas?
Die Sprachkonstrukte waren also vergleichsweise wenige, beim Einsatz war ich schon immer sehr sparsam und habe mich auf die Position zur&#xFC;ckgezogen:
In der Regel brauche ich keine Nebenl&#xE4;ufigkeit und wenn, dann erledigt das entweder ein Framework f&#xFC;r mich (z.B. Web-Container verteilt Requests an Prozesse) oder - wenn sich Nebel&#xE4;ufigkeit und Threads nicht vermeiden lassen, dann konzentriere ich mich darauf, Fachlogik von Synchronisation und Threading schon im Objektmodell eindeutig zu trennen, und dann denke ich mal ganz dolle &#xFC;ber die wenigen nebenl&#xE4;ufigen Klassen nach.
Bis vor wenigen Jahren bin ich mit dieser Strategie auch gut gefahren, denn die Gelegenheiten, in den Nebenl&#xE4;ufigkeit bei meinen Programmier- und Beratungsjobs zum Thema wurden, waren wenige, doch seit etwa 3 Jahren stolpere ich immer h&#xE4;ufiger &#xFC;ber Artikel, Vortr&#xE4;ge und Frameworks zu diesem Thema. Ja, ganze Programmiersprachen und -Paradigmen (funktionale Programmierung) prahlen damit, dass sie sich perfekt f&#xFC;r nebenl&#xE4;ufige Prgrammierung eignen. Warum die Renaissance des Themas?
Die Sprachkonstrukte waren also vergleichsweise wenige, beim Einsatz war ich schon immer sehr sparsam und habe mich auf die Position zur&#xFC;ckgezogen:
In der Regel brauche ich keine Nebenl&#xE4;ufigkeit und wenn, dann erledigt das entweder ein Framework f&#xFC;r mich (z.B. Web-Container verteilt Requests an Prozesse) oder - wenn sich Nebel&#xE4;ufigkeit und Threads nicht vermeiden lassen, dann konzentriere ich mich darauf, Fachlogik von Synchronisation und Threading schon im Objektmodell eindeutig zu trennen, und dann denke ich mal ganz dolle &#xFC;ber die wenigen nebenl&#xE4;ufigen Klassen nach.
Bis vor wenigen Jahren bin ich mit dieser Strategie auch gut gefahren, denn die Gelegenheiten, in den Nebenl&#xE4;ufigkeit bei meinen Programmier- und Beratungsjobs zum Thema wurden, waren wenige, doch seit etwa 3 Jahren stolpere ich immer h&#xE4;ufiger &#xFC;ber Artikel, Vortr&#xE4;ge und Frameworks zu diesem Thema. Ja, ganze Programmiersprachen und -Paradigmen (funktionale Programmierung) prahlen damit, dass sie sich perfekt f&#xFC;r nebenl&#xE4;ufige Prgrammierung eignen. Warum die Renaissance des Themas?
"The free lunch is over": Ein Artikel aus dem Jahre 2005 von Herb Sutter.
In diesem Artikel besch&#xE4;ftigt er sich mit Moores "Gesetz" aus dem Jahre 1965: Die Komplexit&#xE4;t integrieter Schaltkreise verdoppelt sich etwa alle 2 Jahre. Dieses Beobachtung gilt 45 Jahre sp&#xE4;ter immer noch. Wenn auch seit 2004 in ver&#xE4;nderter Form...
Vortrag von Intel auf OOP: H&#xE4;tte man auch nach 2004 einfach weiterhin die Taktfrequenz erh&#xF6;ht, damm w&#xE4;re:
- 2007 der Prozessor geschmolzen
- 2009 der Prozessor die Oberfl&#xE4;chentemperatur der Sonne erreicht worden
Ein anderer interessanter Aspekt, den man aus der Kurve erahnen kann: Der Anstieg des Stromverbrauchs ist in etwa gleichzeitig mit der Taktfrequenz konstant geblieben. Folgerung: Erfolgreiche Multi-Core-Programmierung ist aktiver Umweltschutz, weil der Stromverbrauch pro Rechenzyklus bei niedrigerer Taktfrequenz und mehr Kernen deutlich geringer ist als bei h&#xF6;herer Taktfrequenz und einem Kern!
Und weil das nicht nur Herb Sutter erkannt hat, ist das Thema "nebenl&#xE4;ufige Programmierung" (aka Multi-Core-Programming) seit wenigen Jahre auch immer st&#xE4;rker auf Konferenzen vertreten, neue Programmiersprachen nehmen sich des Themas an, und Frameworks mit dem Fokus wachsen wie Pilze aus dem Boden.
Grund Genug f&#xFC;r einen Berater, der von seinem aktuellen Wissen lebt, sich das Thema neu anzuschauen. Ich habe daher vor etwa einem Jahr beschlossen, mir das Thema wieder von Grund auf neu zu erarbeiten.
Goetz von 2006: Praktische Einf&#xFC;hrung in die aktuelle Version der Java-Multi-Threading-Mechanismen und des java.util.concurrent-Pakete
Herlihy & Shavit von 2008: Gr&#xFC;ndliche, aber auch in Teilen sehr theoretische Einf&#xFC;hring in die Programmierung von Synchronisationsmechanismen wie z.B. Locks
W&#xE4;hrend das Goetz-Buch v.a. die Anwendung der Konstrukte beschreibt, konzentriert sich das Herlihy-Buch auf Theorie und Implementierung der Mechanismen.
Goetz von 2006: Praktische Einf&#xFC;hrung in die aktuelle Version der Java-Multi-Threading-Mechanismen und des java.util.concurrent-Pakete
Herlihy & Shavit von 2008: Gr&#xFC;ndliche, aber auch in Teilen sehr theoretische Einf&#xFC;hring in die Programmierung von Synchronisationsmechanismen wie z.B. Locks
W&#xE4;hrend das Goetz-Buch v.a. die Anwendung der Konstrukte beschreibt, konzentriert sich das Herlihy-Buch auf Theorie und Implementierung der Mechanismen.
Punkt eins und zwei sind essentiell, damit wir unser Programm guten Gewissens abliefern k&#xF6;nnen. Die dritte Eigenschaft jedoch ist der eigentliche Grund f&#xFC;r das neue Interesse: Br&#xE4;uchten wir nicht bessere Performance (mehr Logik wird in der gleichen absoluten Zeit abgearbeitet), dann k&#xF6;nnten wir uns zur&#xFC;cklehnen.
Die ersten beiden Eigenschaften sind bei linearen Programmen vergleichsweise leicht zu erreichen. Die "Illusion" des sequenziell ablaufenden Programms macht es uns leicht, Fehler in der Logik zu erkennen (z.B. mit Hilfe von Unit Tests) und zu beseitigen. In Programmen mit mehreren gleichzeitig - oder quasi gleichzeitig - ablaufenden "F&#xE4;den" ist das anders. Schauen wir uns die 3 Aspekte etwas genauer an.
Punkt eins und zwei sind essentiell, damit wir unser Programm guten Gewissens abliefern k&#xF6;nnen. Die dritte Eigenschaft jedoch ist der eigentliche Grund f&#xFC;r das neue Interesse: Br&#xE4;uchten wir nicht bessere Performance (mehr Logik wird in der gleichen absoluten Zeit abgearbeitet), dann k&#xF6;nnten wir uns zur&#xFC;cklehnen.
Die ersten beiden Eigenschaften sind bei linearen Programmen vergleichsweise leicht zu erreichen. Die "Illusion" des sequenziell ablaufenden Programms macht es uns leicht, Fehler in der Logik zu erkennen (z.B. mit Hilfe von Unit Tests) und zu beseitigen. In Programmen mit mehreren gleichzeitig - oder quasi gleichzeitig - ablaufenden "F&#xE4;den" ist das anders. Schauen wir uns die 3 Aspekte etwas genauer an.
Punkt eins und zwei sind essentiell, damit wir unser Programm guten Gewissens abliefern k&#xF6;nnen. Die dritte Eigenschaft jedoch ist der eigentliche Grund f&#xFC;r das neue Interesse: Br&#xE4;uchten wir nicht bessere Performance (mehr Logik wird in der gleichen absoluten Zeit abgearbeitet), dann k&#xF6;nnten wir uns zur&#xFC;cklehnen.
Die ersten beiden Eigenschaften sind bei linearen Programmen vergleichsweise leicht zu erreichen. Die "Illusion" des sequenziell ablaufenden Programms macht es uns leicht, Fehler in der Logik zu erkennen (z.B. mit Hilfe von Unit Tests) und zu beseitigen. In Programmen mit mehreren gleichzeitig - oder quasi gleichzeitig - ablaufenden "F&#xE4;den" ist das anders. Schauen wir uns die 3 Aspekte etwas genauer an.
Was wir uns zun&#xE4;chst klarmachen m&#xFC;ssen: inc() ist f&#xFC;r den Prozessor - und auch f&#xFC;r die JVM - keine atomare Operation...
Laufen die beiden Aufrufe hintereinander ab, dann ist alles wunderbar.
Wenn es jedoch zu Verschachtelungen kommt, dann kann so etwas triviales wie das Hochz&#xE4;hlen schon zu falschen Ergebnissen f&#xFC;hren.
In Zeiten von mehreren Kernen wird das Problem &#xFC;brigens pl&#xF6;tzlich viel relevanter. Bei einem Kern konnte das nur passieren, wenn zwischen den Aufrufen ein Threadwechsel stattgefunden hat, was vergleichsweise selten passiert. Bei tats&#xE4;chlich gleichzeitiger Ausf&#xFC;hrung ist der Konflikt um mehr als eine Gr&#xF6;&#xDF;enordnung wahrscheinlicher.
Hier der allgemeine Fall. Und das ist zudem noch vereinfacht, weil tats&#xE4;chlich nicht nur die Reihenfolge zwischen den Threads nicht bekannt ist, sonder tats&#xE4;chlich auch die Reihenfolge IN EINEM THREAD anders sein kann, als es unser Programm nahelegt.
Diese Umsortierung der einzelnen Schritte macht der Compiler - oder eventuell sogar der Prozessor - um die Ausf&#xFC;hrungsgeschwindigkeit von Programmen zu optimieren. In einem sequenziellen Programm bekommen wir jedoch davon nichts mit, weil es f&#xFC;r uns als Programmierer alles so arrangiert wird, dass wir es nicht mitbekommen! Wir leben in unserer eigenen kleinen Trueman-Show.
Was bedeutet "korrektes Verhalten" bei nebenl&#xE4;ufiger Ausf&#xFC;hrung?
In einem sequenziellen Programm sollte ein Objekt zwischen Methoden-Aufrufen einen sinnvollen Zustand haben. In einem nebenl&#xE4;ufigen Programm tritt dieses "dazwischen" m&#xF6;glicherweise nie ein. Stellen wir uns ein mengenwertiges Objekt vor, in das st&#xE4;ndig Objekte eingef&#xFC;gt und gel&#xF6;scht werden. Welche Objekte sollen "korrekterweise" zu einem bestimmten Zeitpunkt enthalten sein? Was ist die korrekte Gr&#xF6;&#xDF;e (size) dieses Objekts zu einem bestimmten Zeitpunkt?
Die Frage ist nicht allgemein zu beantworten, siehe "The Art of Multiprocessor Programming"
Locks sind unser Mittel um die sequenzielle Illusion wieder so weit wie n&#xF6;tig aufrecht erhalten zu k&#xF6;nnen.
Synchronized ist nichts anderes als Locking, bei dem this als Lock-Objekt benutzt wird.
Warum ben&#xF6;tigt die value()-Methode ein Lock?
Locks haben neben der Sequenzialisierung eine zweite erw&#xFC;nschte Wirkung; sie sorgen f&#xFC;r die Publizierung des Objektzustandes (publishing). W&#xE4;re das nicht so, dann m&#xFC;sste die Variable count ein volatile-Attribut bekommen. Zu dem Thema kommen wir gleich noch...
Deadlocks: Zu diesem Thema kommen wir noch.
Sequenzialisierte Programmausf&#xFC;hrung verhindert die Performance-Verbesserung. W&#xE4;re dies nicht so, dann k&#xF6;nnten wir f&#xFC;r alle Methoden ein globales Lock-Objekt zur Synchronisation verwenden, und unsere Nebenl&#xE4;ufigkeits-Probleme w&#xE4;ren gel&#xF6;st!
TAS: test and set
CAS: compare and swap
Deadlocks: Zu diesem Thema kommen wir noch.
Sequenzialisierte Programmausf&#xFC;hrung verhindert die Performance-Verbesserung. W&#xE4;re dies nicht so, dann k&#xF6;nnten wir f&#xFC;r alle Methoden ein globales Lock-Objekt zur Synchronisation verwenden, und unsere Nebenl&#xE4;ufigkeits-Probleme w&#xE4;ren gel&#xF6;st!
TAS: test and set
CAS: compare and swap
Deadlocks: Zu diesem Thema kommen wir noch.
Sequenzialisierte Programmausf&#xFC;hrung verhindert die Performance-Verbesserung. W&#xE4;re dies nicht so, dann k&#xF6;nnten wir f&#xFC;r alle Methoden ein globales Lock-Objekt zur Synchronisation verwenden, und unsere Nebenl&#xE4;ufigkeits-Probleme w&#xE4;ren gel&#xF6;st!
TAS: test and set
CAS: compare and swap
Die Konsistenz zwischen den verschiedenen Caches und dem "echten" Speicher wird &#xFC;ber sogenannte Cache Coherence Protokolle sichergestellt. Auf Maschinencode-Ebene werden so genannte Memory Barriers verwendet, um die Reihenfolge von Speicherzugriffen bestimmen zu k&#xF6;nnen.
Schlussfolgerung: Jede Operation, die (potenziell) Zugriff auf gemeinsamen Speicher ausf&#xFC;hrt, ist Faktor 100 langsamer als eine rein thread-lokale Operation. Diese ist eine Angabe &#xFC;ber den relativen Unterschied und hat sich in den letzten 20 Jahren kaum ge&#xE4;ndert.
Die Konsistenz zwischen den verschiedenen Caches und dem "echten" Speicher wird &#xFC;ber sogenannte Cache Coherence Protokolle sichergestellt. Auf Maschinencode-Ebene werden so genannte Memory Barriers verwendet, um die Reihenfolge von Speicherzugriffen bestimmen zu k&#xF6;nnen.
Schlussfolgerung: Jede Operation, die (potenziell) Zugriff auf gemeinsamen Speicher ausf&#xFC;hrt, ist Faktor 100 langsamer als eine rein thread-lokale Operation. Diese ist eine Angabe &#xFC;ber den relativen Unterschied und hat sich in den letzten 20 Jahren kaum ge&#xE4;ndert.
Die Konsistenz zwischen den verschiedenen Caches und dem "echten" Speicher wird &#xFC;ber sogenannte Cache Coherence Protokolle sichergestellt. Auf Maschinencode-Ebene werden so genannte Memory Barriers verwendet, um die Reihenfolge von Speicherzugriffen bestimmen zu k&#xF6;nnen.
Schlussfolgerung: Jede Operation, die (potenziell) Zugriff auf gemeinsamen Speicher ausf&#xFC;hrt, ist Faktor 100 langsamer als eine rein thread-lokale Operation. Diese ist eine Angabe &#xFC;ber den relativen Unterschied und hat sich in den letzten 20 Jahren kaum ge&#xE4;ndert.
Die Sequenzialisierung ist jedoch nicht das einzige Problem, das wir uns bei Multi-Core-Architekturen einhandeln...
Warum m&#xFC;ssen wir uns dar&#xFC;ber Gedanken machen?
Zur&#xFC;ck zu unserem vereinfachten Hardware-Schaubild.
1. Ein Objekt, das nicht von mehreren Threads zugegriffen wird, muss im besten Fall lediglich im Prozessor "leben" uns sichtbar sein. Und genau daf&#xFC;r optimieren die Compiler unseren Code! Nur manchmal, z.B. bei einem Kontext-Switch, ist die VM gezwungen, das Objekt tats&#xE4;chlich in den Hauptspeicher zu bringen.
2. Ein Objekt, das von mehreren Threads zugegriffen wird, muss jedoch &#xFC;berall sichtbar sein - und das m&#xF6;glichst sofort und nach jeder &#xC4;nderung. Die VM wei&#xDF; aber ohne unsere Mithilfe nicht, wann wir ein Objekt auch in anderen Threads verwenden wollen. Diese Aufgabe nennt sich "Publishing"
Korrektes "Publishing" von Objekten und ihres Zustands ist ein eigenes, nicht-triviales Thema f&#xFC;r sich, das ich heute nur ganz am Rande ein paar mal streife. Wer sich im Detail damit besch&#xE4;ftigen m&#xF6;chte, dem steht ein harter Kampf mit Javas Memory-Modell bevor...
Zur&#xFC;ck zu unserem vereinfachten Hardware-Schaubild.
1. Ein Objekt, das nicht von mehreren Threads zugegriffen wird, muss im besten Fall lediglich im Prozessor "leben" uns sichtbar sein. Und genau daf&#xFC;r optimieren die Compiler unseren Code! Nur manchmal, z.B. bei einem Kontext-Switch, ist die VM gezwungen, das Objekt tats&#xE4;chlich in den Hauptspeicher zu bringen.
2. Ein Objekt, das von mehreren Threads zugegriffen wird, muss jedoch &#xFC;berall sichtbar sein - und das m&#xF6;glichst sofort und nach jeder &#xC4;nderung. Die VM wei&#xDF; aber ohne unsere Mithilfe nicht, wann wir ein Objekt auch in anderen Threads verwenden wollen. Diese Aufgabe nennt sich "Publishing"
Korrektes "Publishing" von Objekten und ihres Zustands ist ein eigenes, nicht-triviales Thema f&#xFC;r sich, das ich heute nur ganz am Rande ein paar mal streife. Wer sich im Detail damit besch&#xE4;ftigen m&#xF6;chte, dem steht ein harter Kampf mit Javas Memory-Modell bevor...
aus Goetz06, S. 49ff
Wann habe ich ein Objekt so erzeugt, dass es vollst&#xE4;ndig in allen Threads sichtbar ist? Vollst&#xE4;ndig hei&#xDF;t, dass nicht nur das Objekt selbst, sondern auch sein innerer Zustand in allen Threads sichtbar ist.
Unver&#xE4;nderlich: alle Felder sind final. Dann ist das Objekt publiziert, auch wenn die Referenz darauf nicht final/volatile/atomic ist. Fallstrick: Leak object in constructor.
Locking beim Zugriff: Auch, wenn du nur lesend zugreifst!
Locking beim Zugriff: Auch, wenn du nur lesend zugreifst!
Locking beim Zugriff: Auch, wenn du nur lesend zugreifst!
Locking beim Zugriff: Auch, wenn du nur lesend zugreifst!
So genannter lock-ordering deadlock: Wenn Locks in der falschen Reihenfolge angefordert werden, kann es zur Verklemmung kommen.
Ein praxisn&#xE4;heres Deadlock-Beispiel werden wir sp&#xE4;ter noch sehen.
Es gibt auch Livelocks: Ein Thread, obwohl er nicht blockiert ist, versucht immer wieder die gleiche Operation auszuf&#xFC;hren, die immer wieder fehlschl&#xE4;gt. Passiert z.B. wenn in einem transaktionalen Message-orientierten System eine Nachricht nicht abgearbeitet werden kann, und nach dem Rollback der Transaktion die gleiche Nachricht wieder an den Anfang der Queue gesetzt wird.
Gr&#xFC;nde:
- Die Ressource ist ausgelastet.
- Der Verteilungsmechanismus ist nicht fair. Locks sind in der Regel nicht fair implementiert, weil faire Implementierungen eine schlechtere Performance haben!
Responsiveness: Vor allem in GUI-Applikationen von Bedeutung. Auch mit einem Kern zu erreichen.
Erfordert v.a. thread-sicheres Programmieren, damit das Starten von parallelen Threads gefahrlos m&#xF6;glich ist.
Bei Swing-Applikationen muss man aufpassen, weil Swing von sich aus nicht thread-safe ist und Oberfl&#xE4;chenmanipulationen in AWT-Thread erfolgen m&#xFC;ssen. F&#xFC;r die Thread-Sicherheit der darunterliegenden Fachobjekte muss man als Entwickler selbst sorgen!
Throughput: Dies ist tats&#xE4;chlich nur bei mehreren Kernen/Prozessoren sinnvoll.
Eine grunds&#xE4;tzliche Aussage dazu macht Amdahls Law...
AL liefert eine obere Grenze f&#xFC;r die erreichbare Beschleunigung eines Programms.
Vereinfachte Annahme: Ein (Teil-)Programm ist in einen beliebig parallelisierbaren und in einen nicht parallelisierbaren Anteil aufteilbar.
Dann wird die maximal erreichbare Beschleunigung durch den nicht parallelisierbaren Anteil begrenzt.
Hier die sehr optimistische Annahme, dass nur 20% des Programms nicht parallelisierbar sind. Dann kann man auch mit unendlich vielen Prozessoren nur auf eine Beschleunigung des Faktors 5 kommen. Und das ist das ist die maximale Obergrenze der Beschleunigung!
Aus einem linearen Algorithmus einen parallelen zu machen ist ein nicht-triviales Problem f&#xFC;r das es keine allgemeine L&#xF6;sung gibt.
Zwei Standardstrategien existieren.
Aus einem linearen Algorithmus einen parallelen zu machen ist ein nicht-triviales Problem f&#xFC;r das es keine allgemeine L&#xF6;sung gibt.
Zwei Standardstrategien existieren.
Strategie 1: Wir schauen welche Schritte des Algorithmus unabh&#xE4;ngig voneinander sind und parallel abgewickelt werden k&#xF6;nnen. Daf&#xFC;r gibt es auch Formalisierungsmechanismen &#xFC;ber Abh&#xE4;ngigkeitsgrafen und Petrinetze.
Klassisches Lehrbuchbeispiel ist Kochrezept. Erfordert sehr viel Handarbeit und skaliert meist nicht f&#xFC;r eine hohe Anzahl von Prozessoren.
Strategie 2: Ersetzen des Algorithmus oder eines Teils davon durch einen rekursiv zerlegbaren Algorithmus.
Die optimale Anzahl der Zerlegungen ist dann abh&#xE4;ngig von Anzahl der Kerne und der Kosten f&#xFC;r Zerlegung und Zusammenbringung => Fork/Join
Diese Strategie skaliert meist auch f&#xFC;r eine hohe Anzahl von Kernen, aber sie existiert nicht f&#xFC;r alle Probleme.
Datenparallelisierung ist manchmal sehr einfach, wenn die Berechnung am einzelnen Datenelement unabh&#xE4;ngig von den anderen Datenelementen ist.
Map-Reduce ist einer der Ans&#xE4;tze, die hier eine Rolle spielen.
Sowohl bei Kontrollfluss-Parallelisierung als auch bei Datenparallelisierung spielen Safety und Liveness eine Rolle, sobald die nebenl&#xE4;ufig arbeitenden Einheiten auf gemeinsame, ver&#xE4;nderbare Objekte zugreifen. Bei Datenparallelisierung versucht man jedoch idR das Schneiden der Daten so vorzunehmen, dass es keinen gemeinsam genutzten Zustand mehr gibt.
Mit Version 5 wurde Java erweitert um drei Pakete, die sich dem Thema parallele Programmierung widmen.
Im Gro&#xDF;en und Ganzen enth&#xE4;lt j.u.concurrent die Erweiterungen, die Doug Lean schon als zus&#xE4;tzliches Paket vor 10 Jahren geschaffen hat, das dem Java-Programmierer die allt&#xE4;glichen Aufgaben im Zusammenhang mit Multi-Threading und Synchronisation erleichtern sollen.
Explizite Locks und Conditions
Muster zur Verwendung von expliziten Locks: try-finally.
Muster zur Verwendung von expliziten Locks: try-finally.
Muster zur Verwendung von expliziten Locks: try-finally.
Impizite Locks sind leichter zu verwenden, da man sich das try-finally sparen kann.
Die Klasse Executors bietet zahlreiche M&#xF6;glichkeiten, sich ExecutorService-Objekte zu erzeugen. Die gezeigte Variante benutzt Threads wieder, die nicht mehr ben&#xF6;tigt werden, expandiert aber die Anzahl der Threads wenn n&#xF6;tig.
Ein Future ist eine Referenz auf ein zuk&#xFC;nftiges Ergebnis einer Berechnung. Das submit ist nicht blockierend, aber das get() blockiert bis der Task erledigt wurde.
new Thread sollte man eigentlich nur noch benutzen, wenn man sich seinen eigenen Executor baut, oder wenn man in Thread-Pools eigene Thread-Factories ben&#xF6;tigt, z.B. f&#xFC;r Testing oder Profiling.
Die Klasse Executors bietet zahlreiche M&#xF6;glichkeiten, sich ExecutorService-Objekte zu erzeugen. Die gezeigte Variante benutzt Threads wieder, die nicht mehr ben&#xF6;tigt werden, expandiert aber die Anzahl der Threads wenn n&#xF6;tig.
Ein Future ist eine Referenz auf ein zuk&#xFC;nftiges Ergebnis einer Berechnung. Das submit ist nicht blockierend, aber das get() blockiert bis der Task erledigt wurde.
new Thread sollte man eigentlich nur noch benutzen, wenn man sich seinen eigenen Executor baut, oder wenn man in Thread-Pools eigene Thread-Factories ben&#xF6;tigt, z.B. f&#xFC;r Testing oder Profiling.
Die Klasse Executors bietet zahlreiche M&#xF6;glichkeiten, sich ExecutorService-Objekte zu erzeugen. Die gezeigte Variante benutzt Threads wieder, die nicht mehr ben&#xF6;tigt werden, expandiert aber die Anzahl der Threads wenn n&#xF6;tig.
Ein Future ist eine Referenz auf ein zuk&#xFC;nftiges Ergebnis einer Berechnung. Das submit ist nicht blockierend, aber das get() blockiert bis der Task erledigt wurde.
new Thread sollte man eigentlich nur noch benutzen, wenn man sich seinen eigenen Executor baut, oder wenn man in Thread-Pools eigene Thread-Factories ben&#xF6;tigt, z.B. f&#xFC;r Testing oder Profiling.
Oben: Die "alte"Variante, um den Counter thread-sicher zu machen.
Warum gen&#xFC;gt ein volatile bei count nicht? Weil der Prozessor f&#xFC;r das Inkrementieren eines long-Werts im Speicher mehrere Operationen ben&#xF6;tigt.
...
Java bietet jetzt "atomare Container" f&#xFC;r Integers, Longs und Booleans an, mit einer Menge von atomaren Operationen, z.B. incrementAndGet(). Diese atomaren funktionen basieren auf CAS. Wie funktioniert das?
Die tats&#xE4;chliche Implementierung ist wesentlich effizienter, denn sie beruht auf den entsprechenden primitiven Operationen des Prozessors und ist nicht-blockierend.
Atomics k&#xF6;nne auch f&#xFC;r komplexere Synchronisationsprobleme verwendet werden. Wir werden sp&#xE4;ter ein Beispiel f&#xFC;r die Verwendung sehen.
Regel aus Goetz06: In den meisten F&#xE4;llen sind Atomics die bessere Alternative f&#xFC;r volatile, denn nur selten gen&#xFC;gt uns das ordungsgem&#xE4;&#xDF;e Publizieren eines Objekts, meistens m&#xFC;ssen wir auch noch weitergehend synchronisieren und das geht h&#xE4;ufig mit atomics besser.
Schauen wir uns den praktischen Einsatz an meinem Lieblingsbeispiel, dem Konto, an.
Testfall erzeugt parallel 100 Konten und greift gleich wieder darauf zu.
ParallelTestCase ist eine spezielle Testklasse f&#xFC;r nebenl&#xE4;ufiges Testen.
Test schl&#xE4;gt mit unterschiedlichen Fehler fehl, z.B. "No account with number 99"...
Der Grund ist wohl die nicht atomare Ausf&#xFC;hrung von "++lastAccountNumber". Daran w&#xFC;rde ein "volatile" nat&#xFC;rlich auch nichts &#xE4;ndern.
Umstellung auf AtomicLong beseitigt zwar den ersten Fehler f&#xFC;hrt aber zu einem anderen....
Dies liegt daran, dass die Klasse HashMap nicht thread-safe ist und dadurch manchmal schon auf Werte zugegriffen wird, deren Key bereits existiert, deren Value aber noch nicht sichtbar ist.
Benutzung einer thread-sicheren Map-Implementierung bringt uns endlich den erhofften gr&#xFC;nen Balken.
Wenn wir jedoch die korrekte Ver&#xF6;ffentlich unserer beiden Members sicherstellen wollen, dann m&#xFC;ssen wir diese auch noch final machen. Diese Ungenauigkeit hat unser Test &#xFC;brigens nicht entdeckt. Das ist typisch: Viele Nebenl&#xE4;ufigkeitsprobleme sind zun&#xE4;chst mal nur potenzielle Probleme und f&#xFC;hren erst bei einem Plattformwechsel oder dem Einsatz zus&#xE4;tzlicher Prozessoren oder einem anderen Nutzungsprofil zu tats&#xE4;chlichen Fehlern im Programmverhalten.
Die Alternative zu dieser L&#xF6;sung w&#xE4;re eine explizite oder implizite Synchronisation der beiden Methoden. Dies w&#xE4;re auch korrekt, sogar ohne final-Modifier, w&#xE4;re aber bei starker Contention vermutlich langsamer, denn unsere jetzige Implementierung verzichtet vollst&#xE4;ndig auf blockierende Synchronisation, denn unter der Haube benutzt ConcurrentHashMap auch Atomics.
Widmen wir uns dem Account und erm&#xF6;glichen nebenl&#xE4;ufiges Einzahlen und Abheben.
Hier sehen wir den typischen Einsatz der compareAndSet-Methode zur Synchronisation von Zugriffen auf ein komplexes Objekt.
Im Falle der Kollision zweier Zugriffe, f&#xFC;hren wir die Operation einfach nochmal durch. Das ist performance-m&#xE4;&#xDF;ig sehr geschickt, da lediglich bei echter Contention zus&#xE4;tzlicher Code ausgef&#xFC;hrt wird.
Diesen Ansatz kann man auch verfolgen, wenn man mehr als eine Member-Variable atomar zugreifen m&#xF6;chte, indem man die zusammengeh&#xF6;rigen Variablen in eine Value-Klasse zusammenfasst und die Referenz darauf &#xFC;ber eine AtomicReference sch&#xFC;tzt.
Diese L&#xF6;sung hat aber auch einen Nachteil: Dieser interne Sync-Mechanismus kann nicht von au&#xDF;en benutzt werden, wenn man beispielsweise atomare Operationen au&#xDF;erhalb der Klasse anbieten m&#xF6;chte, oder wenn man mehrere Accounts gleichzeitig bearbeiten m&#xF6;chte, z.B. bei einer &#xDC;berweisung. Daher w&#xE4;hlen wir in diesem Fall die klassische synchronized-Variante.
Auch hier wieder daran denken, dass auch getBalance() durch synchronized gesch&#xFC;tzt werden muss.
Kommen wir zur &#xDC;berweisung...
Fachlich wollen wir das hier: Eine Klasse, der wir Betrag und Kontonummern geben k&#xF6;nnen und die dann auf Befehl die &#xDC;berweisung durchf&#xFC;hrt, bzw. eine entsprechende Exception wirft, falls der &#xDC;berweisungsbetrag nicht gedeckt ist.
Als Profis erkennen wir nat&#xFC;rlich gleich, dass die execute-Methode nicht thread-sicher ist. Da wir sicherstellen wollen, dass eine &#xDC;berweisung ein atomarer Vorgang ist.
Und wir erkennen auch, dass das einfache synchronized hier nicht gen&#xFC;gt, weil wir ja nicht auf MoneyTransfer synchronisieren m&#xFC;ssen, sondern auf die beteiligten Konten.
Unser MoneyTransfer sieht also so aus.
Sobald wir gleichzeitig mehr als einen Lock halten, m&#xFC;ssen wir nach potenziellen Deadlocks Ausschau halten.
Ich habe vorhin den klassischen Deadlock durch inkonsistente Locking-Reihenfolge gezeigt. Der trifft hier nicht zu, denn wir locken ja konsistent immer zuerst das Quell-Konto und dann das Ziel-Konto.
... Oder vielleicht doch nicht?
Wir m&#xFC;ssen einen Mechanismus finden, der daf&#xFC;r sorgt, dass wir Locks immer in der gleichen Reihenfolge anfragen!
Hier bietet sich die Kontonummer als Kriterium an, wenn es keinen fachlichen Ordnungsmechnismus gibt, kann man z.B. auch den System-weiten identityHashCode benutzen.
Puh, das war harte Arbeit, und dabei hatten wir ein vergleichsweise einfaches Problem vor uns.
Um systemweit sauber und thread-sicher zu arbeiten, ben&#xF6;tigt man viel internes Wissen und mehrere Dutzend Regeln und Heuristiken.
Wir k&#xF6;nnen zu viele Locks setzen, zu wenige, die falschen. Es gibt hunderte Details, die wir beachten m&#xFC;ssen.
Nach der Lekt&#xFC;re von "Java concurrency in Practice" bleibt das Gef&#xFC;hl zur&#xFC;ck, dass ich
- erstens nicht jeden Aspekt der Problematik verstehe (insbesondere bei Details des Java Memory Modells)
- zweitens nicht alle Regeln konsistent werde umsetzen k&#xF6;nnen, schon gar nicht in einem Team oder &#xFC;ber Team-Grenzen hinweg
Zur obigen Erkenntnis kommen die meisten normal-sterblichen Programmierer, wenn sie sich "ausreichend" mit dem Thema besch&#xE4;ftigt haben.
Wer zu einer anderen Erkenntnis gelangt, hat sich entweder noch nicht ausreichend damit besch&#xE4;ftigt ... oder ist ein L&#xFC;gner.
Als &#xDC;bung empfohlen: Diese klassische Observer-Implementierung thread-sicher machen - und dann den Artikel lesen.
Das Denken in dieser Abstraktion hat zwei Vorteile:
- Es f&#xE4;llt all denen leicht, die bisher v.a. in Objekten und sequenziell ausgef&#xFC;hrten Methoden gedacht haben, also den meisten von uns
- Es ist als Plattform (z.B. als JVM-Implementierungen) leicht zu implementieren, weil es sehr eng verwandt ist mit den hardware- und OS-seitigen Mechanismen.
"Thread-Synchronisation is not composable"
Damit verlieren wir einen der Hauptvorteile von Objektorientierung, die uns ja gerade verspricht, dass wir ein komplexes System aus vielen einfacheren Untersystemen, Objekten, zusammenbauen k&#xF6;nnen, ohne dass wir die Implementierungsdetails der einzelnen Objekte kennen m&#xFC;ssen.
Im Kontext der Nebenl&#xE4;ufigkeit wird die Abstraktion vom Objekt, das seinen inneren Zustand durch Methoden zugreifbar und ver&#xE4;nderbar macht, zu einer "leaky abstraction" (undichte Abstraktion), weil wir pl&#xF6;tzlich Wissen &#xFC;ber die zugrundeliegende Implementierung brauchen, um Objekte miteinander kombinieren zu k&#xF6;nnen.
Ich kann die Frage hier und heute nicht beantworten, aber ich m&#xF6;chte ein paar der Alternativen zeigen, die zur Zeit als Kandidaten gehandelt werden.
Das ist keine vollst&#xE4;ndige Liste, sondern eine Auswahl von Ans&#xE4;tzen, mit denen ich mich besch&#xE4;ftigt habe. Mit manchen mehr, mit anderen weniger.
Es gibt auch hier noch andere, z.B. CSP (Communicating sequential processes)
A: atomic, C: consistent, I: isolated, aber nicht D: durable, alternativ V: visible => ASIV
Einschr&#xE4;nkungen: Transaktionale Bereiche d&#xFC;rfen - au&#xDF;er der Ver&#xE4;nderung von State - keine Seiteneffekte haben!
Hier in Pseudo-Notation.
Das Transfer-Objekt muss nichts dar&#xFC;ber wissen, welche Methoden des Kontos bereits atomar sind.
Hier in Pseudo-Notation.
Das Transfer-Objekt muss nichts dar&#xFC;ber wissen, welche Methoden des Kontos bereits atomar sind.
Hier in Pseudo-Notation.
Das Transfer-Objekt muss nichts dar&#xFC;ber wissen, welche Methoden des Kontos bereits atomar sind.
HTM: Es gibt bislang keinen Prozessor in Serienfertigung, der das Konzept unterst&#xFC;tzt, aber prinzipiell ist das einfacher als man vermuten k&#xF6;nnte, weil viele der Mechanismen, die man f&#xFC;r die Umsetzung ben&#xF6;tigt, bereits in den Cache-Coherence-Algorithmen vorhanden sind.
STM: Die Implementierung von STM in imperativen Sprachen ist aktives Forschungsthema sowohl Akademisch als auch in der industriellen Forschung.
Jede ver&#xE4;ndernde Operation erzeugt eine neue Version des Kontos. Auf diese Weise k&#xF6;nnen beliebig viele Threads auf einem Konto arbeiten, ohne dass sie sich dabei in die Quere k&#xE4;men. Irgendwann jedoch m&#xF6;chten wir den ver&#xE4;nderten Wert eines Kontos wieder als aktuellen Wert festlegen, und damit wir dann wissen, auf welche urspr&#xFC;ngliche Zustands-Version wir aufgesetzt haben, geben wir unserem Account-Objekt noch einen Versionsz&#xE4;hler mit...
Dieses Account-"Objekt" ist jetzt keine Entit&#xE4;t im OO-Sinne mehr, sondern lediglich eine Zustandswert eines Objektes. Die eigentlichen Konto-Identit&#xE4;ten werden in der Bank gehalten. Die Bank muss nun aber eine Schnittstelle zur Verf&#xFC;gung stellen, um den Zustand eines - oder mehrer Konten - aktualisieren zu k&#xF6;nnen...
Der zu synchronisierende Code beschr&#xE4;nkt sich hier auf ein Minimum, n&#xE4;mlich die update-Methode:
Diese bekommt als Parameter ein oder mehrer Konto-Wertobjekte, &#xFC;berpr&#xFC;ft dann, ob sich alle neuen Werte auf die aktuelle Basis-Version beziehen, und wenn ja, dann werden allen Konto-Entit&#xE4;ten neue Konto-Werte zugewiesen und dabei noch die Versionsnummer hochgez&#xE4;hlt. Falls vorher bereits ein oder mehrere Werte ge&#xE4;ndert wurden, bekommt der Aufrufer die R&#xFC;ckmeldung, dass der Update nicht geklappt hat, und dass er es nochmal versuchen kann - wenn er will.
Wie w&#xFC;rde also unsere &#xDC;berweisung in diesem Fall aussehen?
Kein Locking mehr notwendig!
Typisches Muster f&#xFC;r Lock-freie Updates: Wir wiederholen unsere Aktion so lange bis sie erfolgreich ist. Im Normalfall, ohne Kollision, hat der Aufruf keinerlei overhead f&#xFC;r Synchronisation!
So wie ich Account implementiert habe, findet bei jeder Ver&#xE4;nderung des Account-Objektes eine komplette Kopie statt. Das ist nicht sehr effizient - in vielen F&#xE4;llen aber egal! Es ist jedoch m&#xF6;glich, dieses "Kopieren" sehr effizient zu machen, was Speicher und Rechenzeit angeht. Im Allgemeinen gen&#xFC;gt es n&#xE4;mlich, wenn die Kopie einen Verweis auf das "Original" bekommt und lediglich der ver&#xE4;nderte Knoten des Wertebaums ausgetauscht wird. (siehe Persistent Data Structures von Clojure)
Es gibt noch einige andere Ans&#xE4;tze, wie man mit unver&#xE4;nderlichen Werten und echten Funktionen zustandsbehaftete Systeme beschreiben kann. Wer sich daf&#xFC;r interessiert sollte nach dem Stichwort "streams in functional programming" googlen.
Bekannt geworden sind Actors durch Erlang (entstanden bei Ericsson), eine vergleichsweises esoterische Programmiersprache, die nicht zuletzt wegen ihres Aktoren-Modells gerade im Aufwind ist. Die Idee ist, dass das Threading-Modell sehr leichtgewichtig ist, damit man tausende oder auch Millionen von parallelen Aktoren haben kann. Aktoren sind sehr nah an der urspr&#xFC;nglichen Idee von Objekt-Orientierung (aus Simula und sp&#xE4;ter Smalltalk), in der Objekte lediglich &#xFC;ber das Versenden von Nachrichten und den Empfang von Antworten miteinander kommunizieren.
Auch Scala hat eine native Bibliothek f&#xFC;r Threads.
Was man gleich erkennt ist, dass Aktoren nicht f&#xFC;r Probleme geeignet sein k&#xF6;nnen, in denen mehrere Aktoren zu einem Konsens &#xFC;ber gemeinsam benutzte Objekte kommen m&#xFC;ssen. Dann m&#xFC;ssen wir n&#xE4;mlich mit komplizierten Protokollen arbeiten, um den Zustand wiederum zu synchronisieren. Wir alle kennen das Problem bei unserer Adresssynchronisation zwischen Rechner und Handy. Beispiel: Die zustandsbehafteten Konten auf jeweils eigene Aktoren zu verteilen ist eine schlechte Idee, wenn wir gleichzeitig konten&#xFC;bergreifende atomare Operationen anbieten m&#xFC;ssen - wie die &#xDC;berweisung.
Wir k&#xF6;nnen aber von unserem letzten Ansatz der unver&#xE4;nderlichen Account-Objekte ausgehen und schauen, ob wir dort einen sinnvollen Einsatz f&#xFC;r Aktoren finden...
Hier ein gpars-Actor:
- Muss die act-Methode implementieren
- Loop() hei&#xDF;t, dass jede Nachricht auf die gleiche Weise behandelt wird
- im react-Block habe ich Zugriff auf das ankommende Nachricht-Objekt. Hier mache ich nur einen dispatch anhand des Nachrichtentyps
Die Implementierung der eigentlichen Fachlichkeit kommt jetzt vollst&#xE4;ndig ohne Synchronisation aus.
Da wir lediglich Nachrichten verschicken k&#xF6;nne wir nicht einfach einen Wert zur&#xFC;ckgeben, sondern m&#xFC;ssen hier "reply" verwenden.
Jetzt k&#xF6;nnen wir einen Actor definieren, um &#xDC;berweisungen auszuf&#xFC;hren.
Hier finden mehrere quasi-synchrone Aufrufe statt (send and wait). Das st&#xF6;rt jedoch die Skalierbarkeit nicht wirklich, weil wir sehr viele dieser Aktoren gleichzeitig starten k&#xF6;nnen!
Unabh&#xE4;ngigkeit: Leichtere Testbarkeit und h&#xF6;heres Abstraktionslevel
Skalierbar: Wenn geschickt implementiert
Verteilbar: Kommunikation lediglich &#xFC;ber asynchrone Nachrichten
Vermeidung von ... aber nicht Verhinderung!
Aktoren erm&#xF6;glichen den Aufbau extrem verl&#xE4;sslicher Systeme.
Ericsson soll mit Erlang-basierten Telefonanlagen eine Verf&#xFC;gbarkeit von 9 Neunen erreichen.
Probleme, die echte atomare Transaktionen erfordern bieten sich nicht an. Wir kennen alle die Schwierigkeiten bei verteilten Transaktionen (2-Phasen-Commit).
Overhead der asynchronen Kommunikation => Aktoren sind f&#xFC;r nicht-verteilte Anwendungen meist nicht die einfachste Abstraktion.
Gef&#xFC;hlt: Aktoren bekommen im Zusammenhang von Multi-Core-Programmierung mehr Aufmerksamkeit als ihnen zusteht.
Implemetierungen f&#xFC;r die JVM gibt es mindestens ein halbes Dutzend: Neben gpars und der nativen Scala-Bibliothek, u.a. Akka und Jetlang
gpars biete die Iterator-Methoden &#xFC;ber Collections in einer parallelen Variante an: findAll, collect etc.
Aber auch Summierung kann teilweise parallel laufen, da immer paarweise addiert werden kann und das Ergebnis auch wieder paarweise usw.
Schauen wir uns an, was hier passiert...
Nach jedem Bearbeitungsschritt werden alle parallel laufenden Arbeitspakete wieder synchronisiert und das Ergebnis ist eine neue Collection.
Dies ist gleichzeitig ein Vorteil und ein Nachteil:
- Vorteil, weil ich mit jedem Zwischenschritt wieder ganz normal weiterarbeiten kann, sequenziell oder parallel.
- Nachteil, weil diese Synchronisation sowohl die Paralellisierung als auch die Skalierbarkeit auf mehrere Rechner beschr&#xE4;nkt, denn die Arbeitsergebnisse m&#xFC;ssen wieder auf einem Rechner zusammengebracht werden.
.
Diese Einschr&#xE4;nkung hat Google zur Weiterentwicklung dieses Ansatzes gebracht: Map & Reduce lautet das Stichwort.
Schauen wir uns an, was hier passiert...
Nach jedem Bearbeitungsschritt werden alle parallel laufenden Arbeitspakete wieder synchronisiert und das Ergebnis ist eine neue Collection.
Dies ist gleichzeitig ein Vorteil und ein Nachteil:
- Vorteil, weil ich mit jedem Zwischenschritt wieder ganz normal weiterarbeiten kann, sequenziell oder parallel.
- Nachteil, weil diese Synchronisation sowohl die Paralellisierung als auch die Skalierbarkeit auf mehrere Rechner beschr&#xE4;nkt, denn die Arbeitsergebnisse m&#xFC;ssen wieder auf einem Rechner zusammengebracht werden.
.
Diese Einschr&#xE4;nkung hat Google zur Weiterentwicklung dieses Ansatzes gebracht: Map & Reduce lautet das Stichwort.
Was hier passiert ist &#xE4;hnlich den parallelen Collections, insofern als die einzelnen Operationen auf Datens&#xE4;tzen parallel ablaufen k&#xF6;nnen.
Der entscheidende Unterschied ist, dass beliebig viele Map-Schritte hintereinander ablaufen k&#xF6;nnen, ohne dass eine Synchronisation der gesamten Collection erfolgen muss.
Erst ganz am Ende - im Reduce-Schritt - wird das Gesamtergebnis ermittelt. Und auch hier kommt Parallelit&#xE4;t zum Einsatz, da immer zwei Werte in einem Schritt zu einem Zwischenergebnis reduziert werden.
Bei diesem Ansatz k&#xF6;nnen sowohl die Daten als auch die Rechenthreads auf verteilten Rechnern leben. Nur am Anfang und ganz am Ende ist Kommunikation zwischen den Knoten n&#xF6;tig, um die Ergebnisse zusammenzuf&#xFC;hren. Man kann sich vorstellen, dass dieses Vorgehen f&#xFC;r Aufgaben wie Indizierung und Suche in gro&#xDF;en Datenbest&#xE4;nden sehr gut skaliert.
Was hier passiert ist &#xE4;hnlich den parallelen Collections, insofern als die einzelnen Operationen auf Datens&#xE4;tzen parallel ablaufen k&#xF6;nnen.
Der entscheidende Unterschied ist, dass beliebig viele Map-Schritte hintereinander ablaufen k&#xF6;nnen, ohne dass eine Synchronisation der gesamten Collection erfolgen muss.
Erst ganz am Ende - im Reduce-Schritt - wird das Gesamtergebnis ermittelt. Und auch hier kommt Parallelit&#xE4;t zum Einsatz, da immer zwei Werte in einem Schritt zu einem Zwischenergebnis reduziert werden.
Bei diesem Ansatz k&#xF6;nnen sowohl die Daten als auch die Rechenthreads auf verteilten Rechnern leben. Nur am Anfang und ganz am Ende ist Kommunikation zwischen den Knoten n&#xF6;tig, um die Ergebnisse zusammenzuf&#xFC;hren. Man kann sich vorstellen, dass dieses Vorgehen f&#xFC;r Aufgaben wie Indizierung und Suche in gro&#xDF;en Datenbest&#xE4;nden sehr gut skaliert.
Was hier passiert ist &#xE4;hnlich den parallelen Collections, insofern als die einzelnen Operationen auf Datens&#xE4;tzen parallel ablaufen k&#xF6;nnen.
Der entscheidende Unterschied ist, dass beliebig viele Map-Schritte hintereinander ablaufen k&#xF6;nnen, ohne dass eine Synchronisation der gesamten Collection erfolgen muss.
Erst ganz am Ende - im Reduce-Schritt - wird das Gesamtergebnis ermittelt. Und auch hier kommt Parallelit&#xE4;t zum Einsatz, da immer zwei Werte in einem Schritt zu einem Zwischenergebnis reduziert werden.
Bei diesem Ansatz k&#xF6;nnen sowohl die Daten als auch die Rechenthreads auf verteilten Rechnern leben. Nur am Anfang und ganz am Ende ist Kommunikation zwischen den Knoten n&#xF6;tig, um die Ergebnisse zusammenzuf&#xFC;hren. Man kann sich vorstellen, dass dieses Vorgehen f&#xFC;r Aufgaben wie Indizierung und Suche in gro&#xDF;en Datenbest&#xE4;nden sehr gut skaliert.
Die Implementierung mit gpars sieht der vorangegangenen L&#xF6;sung sehr &#xE4;hnlich, unterscheidet sich aber im Detail.
Ich habe hier die Summierung als reduce aufgef&#xFC;hrt. Tats&#xE4;chlich bietet gpars aber bereits eine solche Summierungsmethode von sich aus an.
Im Hintergrund werden hier die Parallelen Arrays aus dem jsr166y benutzt.
Google sagt, dass sie das Patent nur erworben haben, um zu verhindern, dass es ein anderer tut.
Nicht verteilte Umgebung: Parallele Collections sind ein einfacheres Konzept!
Eine Dataflow-Variable hat nur drei Operatoren:
- Erzeugen
- Auf das Binden der Variablen warten
- Die Variable binden
task: Convenience-Methode zum starten eines asynchronen Jobs
Kein Locking, da keine Daten ver&#xE4;ndert werden
Determinismus: Wenn ein Deadlock auftritt, dann finde ich ihn auch mit einem nicht-nebenl&#xE4;ufigen Unit Test
Alles bis auf die Ausgabe des Wertes erfolgt nebenl&#xE4;ufig!
Seiteneffekte: IO, print, exceptions. Man wei&#xDF; nicht, wann die Berechnung ausgef&#xFC;hrt wird.
Scala Dataflow Concurrency DSL von J. Boner auf github
und vermutlich andere, von denen ich noch nicht geh&#xF6;rt habe.