Informatik im Fokus
Herausgeber:
Prof. Dr. O. Günther
Prof. Dr. W. Karl
Prof. Dr. R. Lienhart
Prof. Dr. K. Zeppenfeld
Informatik im Fokus
Rauber, T.; Rünger, G.
Multicore: Parallele
Programmierung. 2008
El Moussaoui, H.; Zeppenfeld, K.
AJAX. 2008
Behrendt, J.; Zeppenfeld, K.
Web 2.0. 2008
Bode, A.; Karl, W.
Multicore-Architekturen. 2008
Thomas Rauber · Gudula Rünger
Multicore:
Parallele Programmierung
123
Vorwort
Nach vielen Jahren stetigen technologischen Fortschritts in
der Mikroprozessorentwicklung stellt die Multicore-Techno-
logie die neueste Entwickungsstufe dar. F¨ hrende Hard-
u
warehersteller wie Intel, AMD, Sun oder IBM liefern seit
2005 Mikroprozessoren mit mehreren unabh¨ngigen Pro-
a
zessorkernen auf einem einzelnen Prozessorchip. Im Jahr
2007 verwendet ein typischer Desktop-PC je nach Ausstat-
tung einen Dualcore- oder Quadcore-Prozessor mit zwei
bzw. vier Prozessorkernen. Die Ank¨ ndigungen der Prozes-
u
sorhersteller zeigen, dass dies erst der Anfang einer l¨nger
a
andauernden Entwicklung ist. Eine Studie von Intel prog-
nostiziert, dass im Jahr 2015 ein typischer Prozessorchip
aus Dutzenden bis Hunderten von Prozessorkernen be-
steht, die zum Teil spezialisierte Aufgaben wie Verschl¨ sse-
u
lung, Grafikdarstellung oder Netzwerkmanagement wahr-
nehmen. Ein Großteil der Prozessorkerne steht aber f¨ r u
Anwendungsprogramme zur Verf¨ gung und kann z.B. f¨ r
u u
B¨ ro- oder Unterhaltungssoftware genutzt werden.
u
Die von der Hardwareindustrie vorgegebene Entwick-
lung hin zu Multicore-Prozessoren bietet f¨ r die Software-
u
VI Vorwort
entwickler also neue M¨glichkeiten, die in der Bereitstel-
o
lung zus¨tzlicher Funktionalit¨ten der angebotenen Soft-
a a
ware liegen, die parallel zu den bisherigen Funktionalit¨ten
a
ausgef¨ hrt werden k¨nnen, ohne dass dies beim Nutzer zu
u o
Wartezeiten f¨ hrt. Diese Entwicklung stellt aber auch einen
u
Paradigmenwechsel in der Softwareentwicklung dar, weg
von der herk¨mmlichen sequentiellen Programmierung hin
o
zur parallelen oder Multithreading-Programmierung. Bei-
de Programmierformen sind nicht neu. Der Paradigmen-
wechsel besteht eher darin, dass diese Programmiertechni-
ken bisher nur in speziellen Bereichen eingesetzt wurden,
nun aber durch die Einf¨ hrung von Multicore-Prozessoren
u
in alle Bereiche der Softwareentwicklung getragen werden
und so f¨ r viele Softwareentwickler eine neue Herausforde-
u
rung entsteht.
Das Ziel dieses Buches ist es, dem Leser einen ers-
ten Einblick in die f¨ r Multicore-Prozessoren geeigneten
u
parallelen Programmiertechniken und -systeme zu geben.
Programmierumgebungen wie Pthreads, Java-Threads und
OpenMP werden vorgestellt. Die Darstellung geht dabei
davon aus, dass der Leser mit Standardtechniken der Pro-
grammierung vertraut ist. Das Buch enth¨lt zahlreiche Hin-
a
weise auf weiterf¨ hrende Literatur sowie neuere Entwick-
u
lungen wie etwa neue Programmiersprachen. F¨ r Hilfe bei
u
der Erstellung des Buches und Korrekturen danken wir J¨rg o
D¨ mmler, Monika Glaser, Marco H¨bbel, Raphael Kunis
u o
und Michael Schwind. Dem Springer-Verlag danken wir f¨ r u
die gute Zusammenarbeit.
Bayreuth, Chemnitz, Thomas Rauber
August 2007 Gudula R¨ nger
u
1
Kurzuberblick
¨
Multicore-Prozessoren
Die Entwicklung der Mikroprozessoren hat in den letzten
Jahrzehnten durch verschiedene technologische Innovatio-
nen immer leistungsst¨rkere Prozessoren hervorgebracht.
a
Multicore-Prozessoren stellen einen weiteren Meilenstein in
der Entwicklung dar.
1.1 Entwicklung der Mikroprozessoren
Prozessorchips sind intern aus Transistoren aufgebaut, de-
ren Anzahl ein ungef¨hres Maß f¨ r die Komplexit¨t und
a u a
Leistungsf¨higkeit des Prozessors ist. Das auf empirischen
a
Beobachtungen beruhende Gesetz von Moore besagt,
dass die Anzahl der Transistoren eines Prozessorchips sich
alle 18 bis 24 Monate verdoppelt. Diese Beobachtung wurde
1965 von Gordon Moore zum ersten Mal gemacht und gilt
nun seit uber 40 Jahren. Ein typischer Prozessorchip aus
¨
dem Jahr 2007 besteht aus ca. 200-400 Millionen Transis-
toren: beispielsweise besteht ein Intel Core 2 Duo Prozessor
2 1 Kurz¨berblick Multicore-Prozessoren
u
aus ca. 291 Millionen Transistoren, ein IBM Cell-Prozessor
aus ca. 250 Millionen Transistoren.
Die Erh¨hung der Transistoranzahl ging in der Ver-
o
gangenheit mit einer Erh¨hung der Taktrate einher. Dies
o
steigerte die Verarbeitungsgeschwindigkeit der Prozesso-
ren und die Taktrate eines Prozessors wurde oftmals als
alleiniges Merkmal f¨ r dessen Leistungsf¨higkeit wahrge-
u a
nommen. Gemeinsam f¨ hrte die Steigerung der Taktrate
u
und der Transistoranzahl zu einer durchschnittlichen j¨hr-
a
lichen Leistungssteigerung der Prozessoren von ca. 55%
(bei Integer-Operationen) bzw. 75% (bei Floating-Point-
Operationen), was durch entsprechende Benchmark-Pro-
gramme gemessen wurde, siehe [32] und www.spec.org f¨ r u
eine Beschreibung der oft verwendeten SPEC-Benchmarks.
Eine Erh¨hung der Taktrate im bisherigen Umfang ist je-
o
doch f¨ r die Zukunft nicht zu erwarten. Dies ist darin be-
u
gr¨ ndet, dass mit einer Erh¨hung der Taktrate auch die
u o
Leistungsaufnahme, also der Energieverbrauch des Prozes-
sors, ansteigt, wobei ein Großteil des verbrauchten Stroms
in W¨rme umgewandelt wird und uber L¨ fter abgef¨ hrt
a u u
¨
werden muss. Das Gesetz von Moore scheint aber bis auf
weiteres seine G¨ ltigkeit zu behalten.
u
Die steigende Anzahl verf¨ gbarer Transistoren wurde
u
in der Vergangenheit f¨ r eine Vielzahl weiterer architekto-
u
nischer Verbesserungen genutzt, die die Leistungsf¨higkeit
a
der Prozessoren erheblich gesteigert hat. Dazu geh¨ren u.a.
o
• die Erweiterung der internen Wortbreite auf 64 Bits,
• die Verwendung interner Pipelineverarbeitung f¨ r die
u
ressourcenoptimierte Ausf¨ hrung aufeinanderfolgender
u
Maschinenbefehle,
• die Verwendung mehrerer Funktionseinheiten, mit de-
nen voneinander unabh¨ngige Maschinenbefehle paral-
a
lel zueinander abgearbeitet werden k¨nnen und
o
1.1 Entwicklung der Mikroprozessoren 3
• die Vergr¨ßerung der prozessorlokalen Cachespeicher.
o
Wesentliche Aspekte der Leistungssteigerung sind also
die Erh¨hung der Taktrate und der interne Einsatz paralle-
o
ler Abarbeitung von Instruktionen, z.B. durch das Duplizie-
ren von Funktionseinheiten. Die Grenzen beider Entwick-
lungen sind jedoch abzusehen: Ein weiteres Duplizieren von
Funktionseinheiten und Pipelinestufen ist zwar m¨glich,
o
bringt aber wegen vorhandener Abh¨ngigkeiten zwischen
a
den Instruktionen kaum eine weitere Leistungssteigerung.
Gegen eine weitere Erh¨hung der prozessoreigenen Taktra-
o
te sprechen mehrere Gr¨ nde [36]:
u
• Ein Problem liegt darin, dass die Speicherzugriffsge-
schwindigkeit nicht im gleichen Umfang wie die Pro-
zessorgeschwindigkeit zunimmt, was zu einer Erh¨hung
o
der Zyklenanzahl pro Speicherzugriff f¨ hrt. So brauch-
u
te z.B. um 1990 ein Intel i486 f¨ r einen Zugriff auf
u
den Hauptspeicher zwischen 6 und 8 Maschinenzyklen,
w¨hrend 2006 ein Intel Pentium Prozessor uber 220 Zy-
a ¨
klen ben¨tigt. Die Speicherzugriffszeiten stellen daher
o
einen kritischen limitierenden Faktor f¨ r eine weitere
u
Leistungssteigerung dar.
• Zum Zweiten wird die Erh¨hung der Transistoranzahl
o
durch eine erh¨hte Packungsdichte erreicht, mit der aber
o
auch eine gesteigerte W¨rmeentwicklung pro Fl¨chen-
a a
einheit verbunden ist. Diese wird zunehmend zum Pro-
blem, da die notwendige K¨ hlung entsprechend aufwen-
u
diger wird.
• Zum Dritten w¨chst mit der Anzahl der Transistoren
a
auch die prozessorinterne Leitungsl¨nge f¨ r den Signal-
a u
transport, so dass die Signallaufzeit eine wichtige Rolle
spielt. Dies sieht man an folgender Berechnung: Ein mit
3GHz getakteter Prozessor hat eine Zykluszeit von 0.33
ns = 0.33 ·10−9 sec. In dieser Zeit kann ein Signal eine
4 1 Kurz¨berblick Multicore-Prozessoren
u
Entfernung von 0.33 ·10−9 s·0.3 · 109m/s ≈ 0.1m zur¨ ck-
u
legen, wenn wir die Lichtgeschwindigkeit im Vakuum als
¨
Signalgeschwindigkeit ansetzen. Je nach Ubergangsme-
dium ist die Signalgeschwindigkeit sogar deutlich nied-
riger. Damit k¨nnen die Signale in einem Takt nur ei-
o
ne relativ geringe Entfernung zur¨ cklegen, so dass der
u
Layout-Entwurf der Prozessorchips entsprechend gestal-
tet werden muss.
Um eine weitere Leistungssteigerung der Prozessoren
im bisherigen Umfang zu erreichen, setzen die Prozessor-
hersteller auf eine explizite Parallelverarbeitung innerhalb
eines Prozessors, indem mehrere logische Prozessoren von
einem physikalischen Prozessor simuliert werden oder meh-
rere vollst¨ndige, voneinander nahezu unabh¨ngige Prozes-
a a
sorkerne auf einen Prozessorchip platziert werden. Der Ein-
satz expliziter Parallelverarbeitung innerhalb eines Prozes-
sors hat weitreichende Konsequenzen f¨ r die Programmie-
u
rung: soll ein Programm von der verf¨ gbaren Leistung des
u
Multicore-Prozessors profitieren, so muss es die verf¨ gba-
u
ren Prozessorkerne entsprechend steuern und effizient aus-
nutzen. Dazu werden Techniken der parallelen Program-
mierung eingesetzt. Da die Prozessorkerne eines Prozes-
sorchips ein gemeinsames Speichersystem nutzen, sind Pro-
grammierans¨tze f¨ r gemeinsamen Adressraum geeignet.
a u
1.2 Parallelit¨t auf Prozessorebene
a
Explizite Parallelit¨t auf Prozessorebene wird durch eine
a
entsprechende Architekturorganisation des Prozessorchips
erreicht.
Eine M¨glichkeit ist die oben erw¨hnte Platzierung
o a
mehrerer Prozessorkerne mit jeweils unabh¨ngigen Ausf¨ h-
a u
rungseinheiten auf einem Prozessorchip, was als Multicore-
1.2 Parallelit¨t auf Prozessorebene
a 5
Prozessor bezeichnet wird. Ein anderer Ansatz besteht
darin, mehrere Kontroll߬ sse dadurch gleichzeitig auf ei-
u
nem Prozessor oder Prozessorkern auszuf¨ hren, dass der
u
Prozessor je nach Bedarf per Hardware zwischen den Kon-
troll߬ ssen umschaltet. Dies wird als simultanes Multi-
u
threading (SMT) oder Hyperthreading (HT) bezeich-
net [43].
Bei dieser Art der Parallelit¨t werden die Kontrollfl¨ sse
a u
oft als Threads bezeichnet. Dieser Begriff und die Unter-
schiede zu Prozessen werden in den folgenden Abschnitten
n¨her erl¨utert; zun¨chst reicht es aus, einen Thread als
a a a
Kontrollfluss anzusehen, der parallel zu anderen Threads
desselben Programms ausgef¨ hrt werden kann.
u
Simultanes Multithreading (SMT)
Simultanes Multithreading basiert auf dem Duplizieren des
Prozessorbereiches zur Ablage des Prozessorzustandes auf
der Chipfl¨che des Prozessors. Zum Prozessorzustand geh¨-
a o
ren die Benutzer- und Kontrollregister sowie der Interrupt-
Controller mit seinen zugeh¨rigen Registern. Damit verh¨lt
o a
sich der physikalische Prozessor aus der Sicht des Be-
triebssystems und des Benutzerprogramms wie zwei lo-
gische Prozessoren, denen Prozesse oder Threads zur
Ausf¨ hrung zugeordnet werden k¨nnen. Diese k¨nnen von
u o o
einem oder mehreren Anwendungsprogrammen stammen.
Jeder logische Prozessor legt seinen Prozessorzustand in
einem separaten Prozessorbereich ab, so dass beim Wech-
sel zu einem anderen Thread kein aufwendiges Zwischen-
speichern des Prozessorzustandes im Speichersystem erfor-
derlich ist. Die logischen Prozessoren teilen sich fast al-
le Ressourcen des physikalischen Prozessors wie Caches,
Funktions- und Kontrolleinheiten oder Bussystem. Die Rea-
lisierung der SMT-Technologie erfordert daher nur eine ge-
6 1 Kurz¨berblick Multicore-Prozessoren
u
ringf¨ gige Vergr¨ßerung der Chipfl¨che. F¨ r zwei logische
u o a u
Prozessoren w¨chst z.B. f¨ r einen Intel Xeon Prozessor die
a u
erforderliche Chip߬che um weniger als 5% [44, 67]. Die
a
gemeinsamen Ressourcen des Prozessorchips werden den
logischen Prozessoren reihum zugeteilt, so dass die logi-
schen Prozessoren simultan zur Verf¨ gung stehen. Treten
u
bei einem logischen Prozessor Wartezeiten auf, k¨nnen die
o
Ausf¨ hrungs-Ressourcen dem anderen logischen Prozessor
u
zugeordnet werden, so dass aus der Sicht des physikali-
schen Prozessors eine verbesserte Nutzung der Ressourcen
gew¨hrleistet ist.
a
Untersuchungen zeigen, dass die verbesserte Nutzung
der Ressourcen durch zwei logische Prozessoren je nach An-
wendungsprogramm Laufzeitverbesserungen zwischen 15%
und 30% bewirken kann [44]. Da alle Ressourcen des Chips
von den logischen Prozessoren geteilt werden, ist beim Ein-
satz von wesentlich mehr als zwei logischen Prozessoren f¨ r
u
die meisten Einsatzgebiete keine weitere signifikante Lauf-
zeitverbesserung zu erwarten. Der Einsatz simultanen Mul-
tithreadings wird daher voraussichtlich auf wenige logische
Prozessoren beschr¨nkt bleiben. Zum Erreichen einer Leis-
a
tungsverbesserung durch den Einsatz der SMT-Technologie
ist es erforderlich, dass das Betriebssystem in der Lage
ist, die logischen Prozessoren anzusteuern. Aus Sicht eines
Anwendungsprogramms ist es erforderlich, dass f¨ r jeden
u
logischen Prozessor ein separater Thread zur Ausf¨ hrung
u
bereitsteht, d.h. f¨ r die Implementierung des Programms
u
m¨ ssen Techniken der parallelen Programmierung einge-
u
setzt werden.
Multicore-Prozessoren
Neue Prozessorarchitekturen mit mehreren Prozessorker-
nen auf einem Prozessorchip werden schon seit vielen Jah-
1.2 Parallelit¨t auf Prozessorebene
a 7
ren als die vielversprechendste Technik zur weiteren Leis-
tungssteigerung angesehen. Die Idee besteht darin, anstatt
eines Prozessorchips mit einer sehr komplexen internen Or-
ganisation mehrere Prozessorkerne mit einfacherer Organi-
sation auf dem Prozessorchip zu integrieren. Dies hat den
zus¨tzlichen Vorteil, dass der Stromverbrauch des Prozes-
a
sorchips dadurch reduziert werden kann, dass vor¨ berge-
u
hend ungenutzte Prozessorkerne abgeschaltet werden [27].
Bei Multicore-Prozessoren werden mehrere Prozessor-
kerne auf einem Prozessorchip integriert. Jeder Prozessor-
kern stellt f¨ r das Betriebssystem einen separaten logischen
u
Prozessor mit separaten Ausf¨ hrungsressourcen dar, die
u
getrennt angesteuert werden m¨ ssen. Das Betriebssystem
u
kann damit verschiedene Anwendungsprogramme parallel
zueinander zur Ausf¨ hrung bringen. So k¨nnen z.B. meh-
u o
rere Hintergrundanwendungen wie Viruserkennung, Ver-
schl¨ sselung und Kompression parallel zu Anwendungs-
u
programmen des Nutzers ausgef¨ hrt werden [58]. Es ist
u
aber mit Techniken der parallelen Programmierung auch
m¨glich, ein rechenzeitintensives Anwendungsprogramm (et-
o
wa aus dem Bereich der Computerspiele, der Bildverar-
beitung oder naturwissenschaftlicher Simulationsprogram-
me) auf mehreren Prozessorkernen parallel abzuarbeiten, so
dass die Berechnungszeit im Vergleich zu einer Ausf¨ hrung
u
auf einem Prozessorkern reduziert werden kann.
Damit k¨nnen auch Standardprogramme wie Textver-
o
arbeitungsprogramme oder Computerspiele zus¨tzliche, im
a
Hintergrund ablaufende Funktionalit¨ten zur Verf¨ gung
a u
stellen, die parallel zu den Haupt-Funktionalit¨ten auf ei-
a
nem separaten Prozessorkern durchgef¨ hrt werden und so-
u
mit f¨ r den Nutzer nicht zu wahrnehmbaren Verz¨gerun-
u o
gen f¨ hren. F¨ r die Koordination der innerhalb einer An-
u u
wendung ablaufenden unterschiedlichen Funktionalit¨ten a
8 1 Kurz¨berblick Multicore-Prozessoren
u
m¨ ssen Techniken der parallelen Programmierung einge-
u
setzt werden.
1.3 Architektur von Multicore-Prozessoren
F¨ r die Realisierung von Multicore-Prozessoren gibt es ver-
u
schiedene Implementierungsvarianten, die sich in der An-
zahl der Prozessorkerne, der Gr¨ße und Anordnung der Ca-
o
ches, den Zugriffm¨glichkeiten der Prozessorkerne auf die
o
Caches und dem Einsatz von heterogenen Komponenten
unterscheiden [37]. Dabei werden zur Zeit drei unterschied-
liche Architekturmodelle eingesetzt, von denen auch Misch-
formen auftreten k¨nnen.
o
Hierarchisches Design
Bei einem hierarchischen De-
sign teilen sich mehrere Pro-
zessorkerne mehrere Caches,
die in einer baumartigen Konfi-
Cache/Speicher
guration angeordnet sind, wo-
bei die Gr¨ße der Caches von
o
den Bl¨ttern zur Wurzel steigt.
a
Cache Cache Die Wurzel repr¨sentiert die
a
Verbindung zum Hauptspei-
cher. So kann z.B. jeder Pro-
Kern Kern Kern Kern
zessorkern einen separaten L1-
Cache haben, sich aber mit
hierarchisches Design anderen Prozessorkernen einen
L2-Cache teilen. Alle Prozes-
Abbildung 1.1. Hierarchi- sorkerne k¨nnen auf den ge-
o
sches Design. meinsamen externen Haupt-
speicher zugreifen, was eine
1.3 Architektur von Multicore-Prozessoren 9
dreistufige Hierarchie ergibt. Dieses Konzept kann auf meh-
rere Stufen erweitert werden und ist in Abbildung 1.1
f¨ r drei Stufen veranschaulicht. Zus¨tzliche Untersyste-
u a
me k¨nnen die Caches einer Stufe miteinander verbinden.
o
Ein hierarchisches Design wird typischerweise f¨ r Server-
u
Konfigurationen verwendet.
Ein Beispiel f¨ r ein hierarchisches Design ist der IBM
u
Power5 Prozessor, der zwei 64-Bit superskalare Prozessor-
kerne enth¨lt, von denen jeder zwei logische Prozessoren
a
durch Einsatz von SMT simuliert. Jeder Prozessorkern hat
einen separaten L1-Cache (f¨ r Daten und Programme ge-
u
trennt) und teilt sich mit dem anderen Prozessorkern einen
L2-Cache (1.8 MB) sowie eine Schnittstelle zu einem ex-
ternen 36 MB L3-Cache. Andere Prozessoren mit hierar-
chischem Design sind die Intel Core 2 Prozessoren und die
Sun UltraSPARC T1 (Niagara) Prozessoren.
Pipeline-Design
Bei einem Pipeline-Design
werden die Daten durch meh-
rere Prozessorkerne schrittwei-
se weiterverarbeitet, bis sie
vom letzten Prozessorkern im
Speichersystem abgelegt wer-
den, vgl. Abbildung 1.2. Hoch-
spezialisierte Netzwerk-Prozes-
soren und Grafikchips arbei-
ten oft nach diesem Prinzip.
Ein Beispiel sind die X10 und
X11 Prozessoren von Xelera-
tor zur Verarbeitung von Netz-
Abbildung 1.2. Pipeline-
werkpaketen in Hochleistungs-
Design.
Routern. Der Xelerator X10q,
10 1 Kurz¨berblick Multicore-Prozessoren
u
eine Variante des X10, enth¨lt z.B. 200 separate Prozes-
a
sorkerne, die in einer logischen linearen Pipeline mitein-
ander verbunden sind. Die Pakete werden dem Prozessor
uber mehrere Netzwerkschnittstellen zugef¨ hrt und dann
u
¨
durch die Prozessorkerne schrittweise verarbeitet, wobei je-
der Prozessorkern einen Schritt ausf¨ hrt. Die X11 Netz-
u
werkprozessoren haben bis zu 800 Pipeline-Prozessorkerne.
Netzwerkbasiertes Design
Bei einem netzwerkbasier-
ten Design sind die Pro-
zessorkerne und ihre lokalen
Caches oder Speicher uber ¨
ein Verbindungsnetzwerk mit
den anderen Prozessorkernen
des Chips verbunden, so dass
der gesamte Datentransfer zwi-
schen den Prozessorkernen uber
¨
das Verbindungsnetzwerk l¨uft,
a
vgl. Abbildung 1.3. Der Ein-
satz eines prozessorinternen
Netzwerkes ist insbesondere
Abbildung 1.3. Netzwerk- dann sinnvoll, wenn eine Viel-
basiertes Design. zahl von Prozessorkernen ver-
wendet werden soll. Ein netz-
werkorientiertes Design wird z.B. f¨ r den Intel Teraflop-
u
Prozessor verwendet, der im Rahmen der Intel Tera-Scale-
Initiative entwickelt wurde, vgl. unten, und in dem 80 Pro-
zessorkerne eingesetzt werden.
Weitere Entwicklungen
Das Potential der Multicore-Prozessoren wurde von vie-
len Hardwareherstellern wie Intel und AMD erkannt und
1.3 Architektur von Multicore-Prozessoren 11
seit 2005 bieten viele Hardwarehersteller Prozessoren mit
zwei oder mehr Kernen an. Ab Ende 2006 liefert Intel
Quadcore-Prozessoren und ab 2008 wird mit der Auslie-
ferung von Octcore-Prozessoren gerechnet. IBM bietet mit
der Cell-Architektur einen Prozessor mit acht spezialisier-
ten Prozessorkernen, vgl. Abschnitt 1.4. Der seit Dezember
2005 ausgelieferte UltraSPARC T1 Niagara Prozessor von
Sun hat bis zu acht Prozessorkerne, von denen jeder durch
den Einsatz von simultanem Multithreading, das von Sun
als CoolThreads-Technologie bezeichnet wird, vier Thre-
ads simultan verarbeiten kann. Damit kann ein UltraS-
PARC T1 bis zu 32 Threads simultan ausf¨ hren. Das f¨ r
u u
2008 angek¨ ndigte Nachfolgemodell des Niagara-Prozessors
u
(ROCK) soll bis zu 16 Prozessorkerne enthalten.
Intel Tera-Scale-Initiative
Intel untersucht im Rahmen des Tera-scale Computing Pro-
grams die Herausforderungen bei der Herstellung und Pro-
grammierung von Prozessoren mit Dutzenden von Prozes-
sorkernen [27]. Diese Initiative beinhaltet auch die Ent-
wicklung eines Teraflop-Prozessors, der 80 Prozessorkerne
enth¨lt, die als 8×10-Gitter organisiert sind. Jeder Prozes-
a
sorkern kann Floating-Point-Operationen verarbeiten und
enth¨lt neben einem lokalen Cachespeicher auch einen Rou-
a
ter zur Realisierung des Datentransfers zwischen den Pro-
zessorkernen und dem Hauptspeicher. Zus¨tzlich kann ein
a
solcher Prozessor spezialisierte Prozessorkerne f¨ r die Ver-
u
arbeitung von Videodaten, graphischen Berechnungen und
zur Verschl¨ sselung von Daten enthalten. Je nach Einsatz-
u
gebiet kann die Anzahl der spezialisierten Prozessorkerne
variiert werden.
Ein wesentlicher Bestandteil eines Prozessors mit ei-
ner Vielzahl von Prozessorkernen ist ein effizientes Verbin-
12 1 Kurz¨berblick Multicore-Prozessoren
u
dungsnetzwerk auf dem Prozessorchip, das an eine variable
Anzahl von Prozessorkernen angepasst werden kann, den
Ausfall einzelner Prozessorkerne toleriert und bei Bedarf
das Abschalten einzelner Prozessorkerne erlaubt, falls diese
f¨ r die aktuelle Anwendung nicht ben¨tigt werden. Ein sol-
u o
ches Abschalten ist insbesondere zur Reduktion des Strom-
verbrauchs sinnvoll.
F¨ r eine effiziente Nutzung der Prozessorkerne ist ent-
u
scheidend, dass die zu verarbeitenden Daten schnell zu den
Prozessorkernen transportiert werden k¨nnen, so dass diese
o
nicht auf die Bereitstellung der Daten warten m¨ ssen. Dazu
u
sind ein leistungsf¨higes Speichersystem und I/O-System
a
erforderlich. Das Speichersystem setzt private L1-Caches
ein, auf die nur von jeweils einem Prozessorkern zugegriffen
werden kann, sowie gemeinsame, evtl. aus mehreren Stufen
bestehende L2-Caches ein, die Daten verschiedener Prozes-
sorkerne enthalten. F¨ r einen Prozessorchip mit Dutzenden
u
von Prozessorkernen muss voraussichtlich eine weitere Stufe
im Speichersystem eingesetzt werden [27]. Das I/O-System
muss in der Lage sein, Hunderte von Gigabytes pro Sekun-
de zum Prozessorchip zu transportieren. Hier arbeitet z.B.
Intel an der Entwicklung geeigneter Systeme.
¨
Tabelle 1.1 gibt einen Uberblick uber aktuelle Multicore-
¨
Prozessoren. Zu bemerken ist dabei, dass der Sun Ul-
traSPARC T1-Prozessor im Gegensatz zu den drei ande-
ren Prozessoren kaum Unterst¨ tzung f¨ r Floating-Point-
u u
Berechnungen bietet und somit uberwiegend f¨ r den Ein-
u
¨
satz im Serverbereich, wie Web-Server oder Applikations-
Server, geeignet ist. F¨ r eine detailliertere Behandlung der
u
Architektur von Multicore-Prozessoren und weiterer Bei-
spiele verweisen wir auf [10, 28].
1.4 Beispiele 13
¨
Tabelle 1.1. Uberblick uber verschiedene Multicore-Prozessoren,
¨
vgl. auch [28].
Intel IBM AMD Sun
Prozessor Core 2 Duo Power 5 Opteron T1
Prozessorkerne 2 2 2 8
Instruktionen 4 4 3 1
pro Zyklus
SMT nein ja nein ja
L1-Cache I/D 32/32 64/32 64/64 16/8
in KB per core
L2-Cache 4 MB 1.9 MB 1 MB 3 MB
shared shared per core shared
Taktrate (GHz) 2.66 1.9 2.4 1.2
Transistoren 291 Mio 276 Mio 233 Mio 300 Mio
Stromverbrauch 65 W 125 W 110 W 79 W
1.4 Beispiele
Im Folgenden wird die Architektur von Multicore-Prozes-
soren anhand zweier Beispiele verdeutlicht: der Intel Core
2 Duo-Architektur und dem IBM Cell-Prozessor.
Intel Core 2 Prozessor
Intel Core 2 bezeichnet eine Familie von Intel-Prozessoren
mit ¨hnlicher Architektur. Die Intel Core-Architektur ba-
a
siert auf einer Weiterentwicklung der Pentium M Prozesso-
ren, die viele Jahre im Notebookbereich eingesetzt wurden.
Die neue Architektur l¨st die bei den Pentium 4 Prozesso-
o
ren noch eingesetzte NetBurst-Architektur ab. Signifikante
Merkmale der neuen Architektur sind:
14 1 Kurz¨berblick Multicore-Prozessoren
u
• eine drastische Verk¨ rzung der internen Pipelines (ma-
u
ximal 14 Stufen anstatt maximal 31 Stufen bei der
NetBurst-Architektur), damit verbunden
• eine Reduktion der Taktrate und damit verbunden auch
• eine deutliche Reduktion des Stromverbrauchs: die Re-
duktion des Stromverbrauches wird auch durch eine
Power-Management-Einheit unterst¨ tzt, die das zeit-
u
weise Ausschalten ungenutzter Prozessorteile erm¨glicht
o
[48] und
• die Unterst¨ tzung neuer Streaming-Erweiterungen (Stre-
u
aming SIMD Extensions, SSE).
Intel Core 2 Prozessoren werden zur Zeit (August 2007)
als Core 2 Duo bzw. Core 2 Quad Prozessoren mit 2 bzw.
4 unabh¨ngigen Prozessorkernen in 65nm-Technologie ge-
a
fertigt. Im Folgenden wird der Core 2 Duo Prozessor kurz
beschrieben [24]. Die Core 2 Quad Prozessoren haben einen
ahnlichen Aufbau, enthalten aber 4 statt 2 Prozessorkerne.
¨
Da die Core 2 Prozessoren auf der Technik des Pentium
M Prozessors basieren, unterst¨ tzen sie kein Hyperthrea-
u
ding. Die allgemeine Struktur der Core 2 Duo Architektur
ist in Abb. 1.4 wiedergegeben. Der Prozessorchip enth¨lt a
zwei unabh¨ngige Prozessorkerne, von denen jeder separate
a
L1-Caches anspricht; diese sind f¨ r Instruktionen (32K) und
u
Daten (32K) getrennt realisiert. Der L2-Cache (4 MB) ist
dagegen nicht exklusiv und wird von beiden Prozessorker-
nen gemeinsam f¨ r Instruktionen und Daten genutzt. Alle
u
Zugriffe von den Prozessorkernen und vom externen Bus auf
den L2-Cache werden von einem L2-Controller behandelt.
F¨ r die Sicherstellung der Koh¨renz der auf den verschie-
u a
denen Stufen der Speicherhierarchie abgelegten Daten wird
ein MESI-Protokoll (Modified, Exclusive, Shared, Invalid)
verwendet, vgl. [17, 47, 59] f¨ r eine detaillierte Erkl¨rung.
u a
Alle Daten- und I/O-Anfragen zum oder vom externen Bus
1.4 Beispiele 15
(Front Side Bus) werden uber einen Bus-Controller gesteu-
¨
ert.
Core 2 Duo Prozessor
Core 0 Core 1
Architektur−Ressourcen Architektur−Ressourcen
Ausführungs−Ressourcen Ausführungs−Ressourcen
L1−Caches (I/O) L1−Caches (I/O)
Cache−Controller Cache−Controller
Power−Management−Controller
L2−Cache mit Controller (shared)
Bus−Interface und −Controller
FrontSideBus
¨
Abbildung 1.4. Uberblick Core 2 Duo Architektur.
Ein wichtiges Element ist die Kontrolleinheit f¨ r den
u
Stromverbrauch des Prozessors (Power Management Con-
troller) [48], die den Stromverbrauch des Prozessorchips
durch Reduktion der Taktrate der Prozessorenkerne oder
durch Abschalten (von Teilen) des L2-Caches reduzieren
kann.
Jeder Prozessorkern f¨ hrt einen separaten Strom von
u
Instruktionen aus, der sowohl Berechnungs- als auch Spei-
cherzugriffsinstruktionen (load/store) enthalten kann. Da-
bei kann jeder der Prozessorkerne bis zu vier Instruktionen
gleichzeitig verarbeiten. Die Prozessorkerne enthalten sepa-
rate Funktionseinheiten f¨ r das Laden bzw. Speichern von
u
16 1 Kurz¨berblick Multicore-Prozessoren
u
Daten, die Ausf¨ hrung von Integeroperationen (durch ei-
u
ne ALU, arithmetic logic unit), Floating-Point-Operationen
sowie SSE-Operationen. Instruktionen k¨nnen aber nur
o
dann parallel zueinander ausgef¨ hrt werden, wenn keine
u
Abh¨ngigkeiten zwischen ihnen bestehen. F¨ r die Steue-
a u
rung der Ausf¨ hrung werden komplexe Schedulingverfah-
u
ren eingesetzt, die auch eine Umordnung von Instruktionen
(out-of-order execution) erlauben, um eine m¨glichst gute
o
Ausnutzung der Funktionseinheiten zu verwirklichen [28].
Laden von Instruktionen
Instruktionsschlange
Mikrocode Dekodiereinheit L2−
ROM Cache
(shared)
Register−Umbenennung und −Allokierung
Umordnungspuffer
Instruktions−Scheduler
ALU ALU ALU Load Store
Branch FPAdd FPMul
MMX/SSE MMX/SSE MMX/SSE
Speicherzugriffspuffer
FPMove FPMove FPMove
L1−Datencache
Abbildung 1.5. Instruktionsverarbeitung und Speicherorganisa-
tion eines Prozessorkerns des Intel Core 2 Prozessors.
Abbildung 1.5 veranschaulicht die Organisation der Ab-
arbeitung von Instruktionen durch einen der Prozessorker-
ne [20]. Jeder der Prozessorkerne l¨dt x86-Instruktionen
a
in eine Instruktionsschlange, auf die die Dekodiereinheit
zugreift und die Instruktionen in Mikroinstruktionen zer-
1.4 Beispiele 17
legt. F¨ r komplexere x86-Instruktionen werden die zu-
u
geh¨rigen Mikroinstruktionen uber einen ROM-Speicher
o ¨
geladen. Die Mikroinstruktionen werden vom Instruktions-
Scheduler freien Funktionseinheiten zugeordnet, wobei die
Instruktionen in einer gegen¨ ber dem urspr¨ nglichen Pro-
u u
grammcode ge¨nderten Reihenfolge abgesetzt werden k¨n-
a o
nen. Alle Speicherzugriffsoperationen werden uber den L1-
¨
Datencache abgesetzt, der Integer-und Floating-Point-Da-
ten enth¨lt.
a
F¨ r Ende 2007 bzw. Anfang 2008 sollen Intel Core 2-
u
Prozessoren mit verbesserter Core-Architektur eingef¨ hrt
u
werden (Codename Penryn). Voraussichtlich f¨ r Ende 2008
u
ist eine neue Generation von Intel-Prozessoren geplant, die
auf einer neuen Architektur basiert (Codename Nehalem).
Diese neuen Prozessoren sollen neben mehreren Prozessor-
kernen (zu Beginn acht) auch einen Graphikkern und einen
Speichercontroller auf einem Prozessorchip integrieren. Die
neuen Prozessoren sollen auch wieder die SMT-Technik (si-
multanes Multithreading) unterst¨ tzen, so dass auf jedem
u
Prozessorkern gleichzeitig zwei Threads ausgef¨ hrt werden
u
k¨nnen. Diese Technik wurde teilweise f¨ r Pentium 4 Pro-
o u
zessoren verwendet, nicht jedoch f¨ r die Core 2 Duo und
u
Quad Prozessoren.
IBM Cell-Prozessor
Der Cell-Prozessor wurde von IBM in Zusammenarbeit
mit Sony und Toshiba entwickelt. Der Prozessor wird u.a.
von Sony in der Spielekonsole PlayStation 3 eingesetzt,
siehe [39, 34] f¨ r ausf¨ hrlichere Informationen. Der Cell-
u u
Prozessor enth¨lt ein Power Processing Element (PPE) und
a
8 Single-Instruction Multiple-Datastream (SIMD) Prozesso-
ren. Das PPE ist ein konventioneller 64-Bit-Mikroprozessor
auf der Basis der Power-Architektur von IBM mit relativ
18 1 Kurz¨berblick Multicore-Prozessoren
u
einfachem Design: der Prozessor kann pro Takt zwei In-
struktionen absetzen und simultan zwei unabh¨ngige Thre-
a
ads ausf¨ hren. Die einfache Struktur hat den Vorteil, dass
u
trotz hoher Taktrate eine geringe Leistungsaufnahme re-
sultiert. F¨ r den gesamten Prozessor ist bei einer Taktrate
u
von 3.2 GHz nur eine Leistungsaufnahme von 60-80 Watt
erforderlich.
Auf der Chip߬che des Cell-Prozessors sind neben dem
a
PPE acht SIMD-Prozessoren integriert, die als SPE (Syn-
ergetic Processing Element) bezeichnet werden. Jedes SPE
stellt einen unabh¨ngigen Vektorprozessor mit einem 256KB
a
großen lokalem SRAM-Speicher dar, der als Local Store
(LS) bezeichnet wird. Das Laden von Daten in den LS und
das Zur¨ ckspeichern von Resultaten aus dem LS in den
u
Hauptspeicher muss per Software erfolgen.
Jedes SPE enth¨lt 128 128-Bit-Register, in denen die
a
Operanden von Instruktionen abgelegt werden k¨nnen. Da
o
auf die Daten in den Registern sehr schnell zugegriffen wer-
den kann, reduziert die große Registeranzahl die Notwen-
digkeit von Zugriffen auf den LS und f¨ hrt damit zu ei-
u
ner geringen mittleren Speicherzugriffszeit. Jedes SPE hat
vier Floating-Point-Einheiten (32 Bit) und vier Integer-
Einheiten. Z¨hlt man eine Multiply-Add-Instruktion als
a
zwei Operationen, kann jedes SPE bei 3.2 GHz Taktrate pro
Sekunde uber 25 Milliarden Floating-Point-Operationen
¨
(25.6 GFlops) und uber 25 Milliarden Integer-Operationen
¨
(25.6 Gops) ausf¨ hren. Da ein Cell-Prozessor acht SPE
u
enth¨lt, f¨ hrt dies zu einer maximalen Performance von
a u
uber 200 GFlops, wobei die Leistung des PPE noch nicht
¨
ber¨ cksichtigt wurde. Eine solche Leistung kann allerdings
u
nur bei guter Ausnutzung der LS-Speicher und effizienter
Zuordnung von Instruktionen an Funktionseinheiten der
SPE erreicht werden. Zu beachten ist auch, dass sich diese
Angabe auf 32-Bit Floating-Point-Zahlen bezieht. Der Cell-
1.4 Beispiele 19
Prozessor kann durch Zusammenlegen von Funktionseinhei-
ten auch 64-Bit Floating-Point-Zahlen verarbeiten, dies re-
sultiert aber in einer wesentlich geringeren maximalen Per-
formance. Zur Vereinfachung der Steuerung der SPEs und
zur Vereinfachung des Schedulings verwenden die SPEs in-
tern keine SMT-Technik.
Die zentrale Verbindungseinheit des Cell-Prozessors ist
ein Bussystem, der sogenannte Element Interconnect Bus
(EIB). Dieser besteht aus vier unidirektionalen Ringver-
bindungen, die eine Wortbreite von 16 Bytes haben und
mit der halben Taktrate des Prozessors arbeiten. Zwei der
Ringe werden in entgegengesetzter Richtung zu den ande-
ren beiden Ringe betrieben, so dass die maximale Latenz
im schlechtesten Fall durch einen halben Ringdurchlauf be-
stimmt wird. F¨ r den Transport von Daten zwischen be-
u
nachbarten Ringelementen k¨nnen maximal drei Transfer-
o
operationen simultan durchgef¨ hrt werden, f¨ r den Zyklus
u u
des Prozessors ergibt dies 16 · 3/2 = 24 Bytes pro Zyklus.
F¨ r die vier Ringverbindungen ergibt dies eine maxima-
u
le Transferrate von 96 Bytes pro Zyklus, woraus bei einer
Taktrate von 3.2 GHz eine maximale Transferrate von uber
¨
300 GBytes/Sekunde resultiert. Abbildung 1.6 zeigt einen
schematischen Aufbau des Cell-Prozessors mit den bisher
beschriebenen Elementen sowie dem Speichersystem (Me-
mory Interface Controller, MIC) und dem I/O-System (Bus
Interface Controller, BIC). Das Speichersystem unterst¨ tzt
u
die XDR-Schnittstelle von Rambus. Das I/O-System un-
terst¨ tzt das Rambus RRAC (Redwood Rambus Access
u
Cell) Protokoll.
Zum Erreichen einer guten Leistung ist es wichtig, die
SPEs des Cell-Prozessors effizient zu nutzen. Dies kann f¨ r
u
spezialisierte Programme, wie z.B. Videospiele, durch di-
rekte Verwendung von SPE-Assembleranweisungen erreicht
werden. Da dies f¨ r die meisten Anwendungsprogramme
u
20 1 Kurz¨berblick Multicore-Prozessoren
u
Synergetic Processing Elements
SPU SPU SPU SPU SPU SPU SPU SPU
LS LS LS LS LS LS LS LS
16B/
Zyklus
EIB (bis 96 B/Zyklus)
L2
MIC BIC
L1 PPU
Dual RRAC I/O
64−Bit Power Architektur XDR
Abbildung 1.6. Schematischer Aufbau des Cell-Prozessors.
zu aufwendig ist, werden f¨ r das Erreichen einer guten
u
Gesamtleistung eine effektive Compilerunterst¨ tzung sowie
u
die Verwendung spezialisierter Programmbibliotheken z.B.
zur Verwaltung von Taskschlangen wichtig sein.
2
Konzepte paralleler
Programmierung
Die Leistungsverbesserung der Generation der Multicore-
Prozessoren wird technologisch durch mehrere Prozessor-
kerne auf einem Chip erreicht. Im Gegensatz zu bisherigen
Leistungsverbesserungen bei Prozessoren hat diese Tech-
nologie jedoch Auswirkungen auf die Softwareentwicklung:
Konnten bisherige Verbesserungen der Prozessorhardware
zu Leistungsgewinnen bei existierenden (sequentiellen) Pro-
grammen f¨ hren, ohne dass die Programme ge¨ndert wer-
u a
den mussten, so ist zur vollen Ausnutzung der Leistung
der Multicore-Prozessoren ein Umdenken hin zur paralle-
len Programmierung notwendig [62].
Parallele Programmiertechniken sind seit vielen Jahren
im Einsatz, etwa im wissenschaftlichen Rechnen auf Paral-
lelrechnern oder im Multithreading, und stellen somit kei-
nen wirklich neuen Programmierstil dar. Neu hingegen ist,
dass durch die k¨ nftige Allgegenw¨rtigkeit der Multicore-
u a
Prozessoren ein Ausbreiten paralleler Programmiertechni-
ken in alle Bereiche der Softwareentwicklung erwartet wird
und diese damit zum R¨ stzeug eines jeden Softwareentwick-
u
lers geh¨ren werden.
o
22 2 Konzepte paralleler Programmierung
Ein wesentlicher Schritt in der Programmierung von
Multicore-Prozessoren ist das Bereitstellen mehrerer Be-
rechnungsstr¨me, die auf den Kernen eines Multicore-Pro-
o
zessors simultan, also gleichzeitig, abgearbeitet werden. Zu-
n¨chst ist die rein gedankliche Arbeit durchzuf¨ hren, einen
a u
einzelnen Anwendungsalgorithmus in solche Berechnungs-
str¨me zu zerlegen, was eine durchaus langwierige, schwie-
o
rige und kreative Aufgabe sein kann, da es eben sehr viele
M¨glichkeiten gibt, dies zu tun, und insbesondere korrekte
o
und effiziente Software resultieren soll.
Zur Erstellung der parallelen Software sind einige Grund-
begriffe und -kenntnisse hilfreich:
• Wie wird beim Entwurf eines parallelen Programmes
vorgegangen?
• Welche Eigenschaften der parallelen Hardware sollen zu
Grunde gelegt werden?
• Welches parallele Programmiermodell soll genutzt wer-
den?
• Wie kann der Leistungsvorteil des parallelen Programms
gegen¨ ber dem sequentiellen bestimmt werden?
u
• Welche parallele Programmierumgebung oder -sprache
soll genutzt werden?
2.1 Entwurf paralleler Programme
Die Grundidee der parallelen Programmierung besteht dar-
in, mehrere Berechnungsstr¨me zu erzeugen, die gleichzei-
o
tig, also parallel, ausgef¨ hrt werden k¨nnen und durch ko-
u o
ordinierte Zusammenarbeit eine gew¨nschte Aufgabe er-
u
ledigen. Liegt bereits ein sequentielles Programm vor, so
spricht man auch von der Parallelisierung eines Program-
mes.
2.1 Entwurf paralleler Programme 23
Zur Erzeugung der Berechnungsstr¨me wird die aus-
o
zuf¨ hrende Aufgabe in Teilaufgaben zerlegt, die auch Tasks
u
genannt werden. Tasks sind die kleinsten Einheiten der Par-
allelit¨t. Beim Entwurf der Taskstruktur eines Program-
a
mes m¨ ssen Daten- und Kontrollabh¨ngigkeiten beachtet
u a
und eingeplant werden, um ein korrektes paralleles Pro-
gramm zu erhalten. Die Gr¨ße der Tasks wird Granu-
o
larit¨t genannt. F¨ r die tats¨chliche parallele Abarbei-
a u a
tung werden die Teilaufgaben in Form von Threads oder
Prozessen auf physikalische Rechenressourcen abgebildet.
Die Rechenressourcen k¨nnen Prozessoren eines Paral-
o
lelrechners, aber auch die Prozessorkerne eines Multicore-
Prozessors sein.
Die Zuordnung von Tasks an Prozesse oder Threads
wird auch als Scheduling bezeichnet. Dabei kann man
zwischen statischem Scheduling, bei dem die Zuteilung
beim Start des Programms festgelegt wird, und dynami-
schem Scheduling, bei dem die Zuteilung w¨hrend der
a
Abarbeitung des Programms erfolgt, unterscheiden. Die
Abbildung von Prozessen oder Threads auf Prozessorker-
ne, auch Mapping genannt, kann explizit im Programm
bzw. durch das Betriebssystem erfolgen. Abbildung 2.1
zeigt eine Veranschaulichung.
Software mit mehreren parallel laufenden Tasks gibt es
in vielen Bereichen schon seit Jahren. So bestehen Server-
anwendungen h¨ufig aus mehreren Threads oder Prozes-
a
sen. Ein typisches Beispiel ist ein Webserver, der mit ei-
nem Haupt-Thread HTTP-Anfragenachrichten von belie-
bigen Clients (Browsern) entgegennimmt und f¨ r jede ein-
u
treffende Verbindungsanfrage eines Clients einen separa-
ten Thread erzeugt. Dieser Thread behandelt alle von die-
sem Client eintreffenden HTTP-Anfragen und schickt die
zugeh¨rigen HTTP-Antwortnachrichten uber eine Client-
o ¨
spezifische TCP-Verbindung. Wird diese TCP-Verbindung
24 2 Konzepte paralleler Programmierung
Prozessor−
Tasks Threads Kerne
T1 T2
Schedu− T1
ling Mapping P1
T2
T3
Thread−
Task−
T3 P2
Zerlegung Zuordnung
Abbildung 2.1. Veranschaulichung der typischen Schritte zur
Parallelisierung eines Anwendungsalgorithmus. Der Algorithmus
wird in der Zerlegungsphase in Tasks mit gegenseitigen Abh¨ngig-
a
keiten aufgespalten. Diese Tasks werden durch das Scheduling
Threads zugeordnet, die auf Prozessorkerne abgebildet werden.
geschlossen, wird auch der zugeh¨rige Thread beendet.
o
Durch dieses Vorgehen kann ein Webserver gleichzeitig vie-
le ankommende Anfragen nebenl¨ufig erledigen oder auf
a
verf¨ gbaren Rechenressourcen parallel bearbeiten. F¨ r Web-
u u
server mit vielen Anfragen (wie google oder ebay) werden
entsprechende Plattformen mit vielen Rechenressourcen be-
reitgehalten.
Abarbeitung paralleler Programme
F¨ r die parallele Abarbeitung der Tasks bzw. Threads oder
u
Prozesse gibt es verschiedene Ans¨tze, vgl. z.B. auch [3]:
a
• Multitasking: Multitasking-Betriebssysteme arbeiten
mehrere Threads oder Prozesse in Zeitscheiben auf dem-
selben Prozessor ab. Hierdurch k¨nnen etwa die Latenz-
o
zeiten von I/O-Operationen durch eine verschachtelte
Abarbeitung der Tasks uberdeckt werden. Diese Form
¨
2.1 Entwurf paralleler Programme 25
der Abarbeitung mehrerer Tasks wird als Nebenl¨ufig-
a
keit (Concurrency) bezeichnet; mehrere Tasks werden
gleichzeitig bearbeitet, aber nur eine Task macht zu ei-
nem bestimmten Zeitpunkt einen tats¨chlichen Rechen-
a
fortschritt. Eine simultane parallele Abarbeitung findet
also nicht statt.
• Multiprocessing: Die Verwendung mehrerer physi-
kalischer Prozessoren macht eine tats¨chliche paralle-
a
le Abarbeitung mehrerer Tasks m¨glich. Bedingt durch
o
die hardwarem¨ßige Parallelit¨t mit mehreren physika-
a a
lischen Prozessoren kann jedoch ein nicht unerheblicher
zeitlicher Zusatzaufwand (Overhead) entstehen.
• Simultanes Multithreading (SMT): Werden meh-
rere logische Prozessoren auf einem physikalischen Pro-
zessor ausgef¨ hrt, so k¨nnen die Hardwareressourcen ei-
u o
nes Prozessors besser genutzt werden und es kann eine
teilweise beschleunigte Abarbeitung von Tasks erfolgen,
vgl. Kap. 1. Bei zwei logischen Prozessoren sind Leis-
tungssteigerungen durch Nutzung von Wartezeiten ei-
nes Threads f¨ r die Berechnungen des anderen Threads
u
um bis zu 30 % m¨glich.
o
• Chip-Multiprocessing: Der n¨chste Schritt ist nun,
a
die Idee der Threading-Technologie auf einem Chip mit
dem Multiprocessing zu verbinden, was durch Multicore-
Prozessoren m¨glich ist. Die Programmierung von Mul-
o
ticore-Prozessoren vereint das Multiprocessing mit dem
simultanen Multithreading in den Sinne, dass Multi-
threading-Programme nicht nebenl¨ufig sondern tat-
a
s¨chlich parallel auf einen Prozessor abgearbeitet wer-
a
den. Dadurch sind im Idealfall Leistungssteigerungen
bis zu 100 % f¨ r einen Dualcore-Prozessor m¨glich.
u o
F¨ r die Programmierung von Multicore-Prozessoren wer-
u
den Multithreading-Programme eingesetzt. Obwohl viele
26 2 Konzepte paralleler Programmierung
moderne Programme bereits Multithreading verwenden,
gibt es doch Unterschiede, die gegen¨ ber der Programmie-
u
rung von Prozessoren mit simultanem Multithreading zu
beachten sind:
• Einsatz zur Leistungsverbesserung: Die Leistungsver-
besserungen von SMT-Prozessoren wird meistens zur
Verringerung der Antwortzeiten f¨ r den Nutzer ein-
u
gesetzt, indem etwa ein Thread zur Beantwortung ei-
ner oder mehrerer Benutzeranfragen abgespalten und
nebenl¨ufig ausgef¨ hrt wird. In Multicore-Prozessoren
a u
hingegen wird die gesamte Arbeit eines Programmes
durch Partitionierung auf die einzelnen Kerne verteilt
und gleichzeitig abgearbeitet.
• Auswirkungen des Caches: Falls jeder Kern eines Mul-
ticore-Prozessors einen eigenen Cache besitzt, so kann
es zu dem beim Multiprocessing bekannten False Sha-
ring kommen. Bei False Sharing handelt es sich um das
Problem, dass zwei Kerne gleichzeitig auf Daten arbei-
ten, die zwar verschieden sind, jedoch in derselben Ca-
chezeile liegen. Obwohl die Daten also unabh¨ngig sind,
a
wird die Cachezeile im jeweils anderen Kern als ung¨ ltig
u
markiert, wodurch es zu Leistungsabfall kommt.
• Thread-Priorit¨ten: Bei der Ausf¨ hrung von Multithrea-
a u
ding-Programmen auf Prozessoren mit nur einem Kern
wird immer der Thread mit der h¨chsten Priorit¨t zu-
o a
erst bearbeitet. Bei Prozessoren mit mehreren Kernen
k¨nnen jedoch auch Threads mit unterschiedlichen Prio-
o
rit¨ten gleichzeitig abgearbeitet werden, was zu durch-
a
aus unterschiedlichen Resultaten f¨ hren kann.
u
Diese Beispiele zeigen, dass f¨ r den Entwurf von Multi-
u
threading-Programmen f¨ r Multicore-Prozessoren nicht nur
u
die Techniken der Threadprogrammierung gebraucht wer-
den, sondern Programmiertechniken, Methoden und Design-
2.2 Klassifizierung paralleler Architekturen 27
entscheidungen der parallelen Programmierung eine erheb-
liche Rolle spielen.
2.2 Klassifizierung paralleler Architekturen
Unter paralleler Hardware wird Hardware zusammenge-
fasst, die mehrere Rechenressourcen bereitstellt, auf denen
ein Programm in Form mehrerer Berechnungsstr¨me abge-
o
arbeitet wird. Die Formen paralleler Hardware reichen also
von herk¨mmlichen Parallelrechnern bis hin zu parallelen
o
Rechenressourcen auf einem Chip, wie z.B. bei Multicore-
Prozessoren. Eine erste Klassifizierung solcher paralleler
Hardware hat bereits Flynn in der nach ihm benannten
Flynnschen Klassifikation gegeben [23]. Diese Klassi-
fikation unterteilt parallele Hardware in vier Klassen mit
unterschiedlichen Daten- und Kontroll߬ ssen:
u
• Die SISD (Single Instruction, Single Data) Rechner be-
sitzen eine Rechenressource, einen Datenspeicher und
einen Programmspeicher, entsprechen also dem klassi-
schen von-Neumann-Rechner der sequentiellen Pro-
grammierung.
• Die MISD (Multiple Instruction, Single Data) Rechner
stellen mehrere Rechenressourcen, aber nur einen Pro-
grammspeicher bereit. Wegen der geringen praktischen
Relevanz spielt diese Klasse keine wesentliche Rolle.
• Die SIMD (Single Instruction, Multiple Data) Rech-
ner bestehen aus mehreren Rechenressourcen mit jeweils
separatem Zugriff auf einen Datenspeicher, aber nur ei-
nem Programmspeicher. Jede Ressource f¨ hrt dieselben
u
Instruktionen aus, die aus dem Programmspeicher gela-
den werden, wendet diese aber auf unterschiedliche Da-
ten an. F¨ r diese Klasse wurden in der Vergangenheit
u
Parallelrechner konstruiert und genutzt.
28 2 Konzepte paralleler Programmierung
• Die MIMD (Multiple Instruction, Multiple Data) Rech-
ner sind die allgemeinste Klasse und zeichnen sich durch
mehrere Rechenressourcen mit jeweils separatem Zugriff
auf einen Datenspeicher und jeweils lokalen Programm-
speichern aus. Jede Rechenressource kann also unter-
schiedliche Instruktionen auf unterschiedlichen Daten
verarbeiten.
Zur Klasse der MIMD-Rechner geh¨ren viele der heu-
o
te aktuellen Parallelrechner, Cluster von PCs aber auch
Multicore-Prozessoren, wobei die einzelnen Prozessoren,
die PCs des Clusters oder die Kerne auf dem Chip ei-
nes Multicore-Prozessors die jeweiligen Rechenressourcen
bilden. Dies zeigt, dass die Flynnsche Klassifizierung f¨ r
u
die heutige Vielfalt an parallelen Rechenressourcen zu grob
ist und weitere Unterteilungen f¨ r den Softwareentwickler
u
n¨ tzlich sind. Eine der weiteren Unterschiede der Hardware
u
von MIMD-Rechnern ist die Speicherorganisation, die sich
auf den Zugriff der Rechenressourcen auf die Daten eines
Programms auswirkt:
Rechner mit verteiltem Speicher bestehen aus Re-
chenressourcen, die jeweils einen ihnen zugeordneten lo-
kalen bzw. privaten Speicher haben. Auf Daten im loka-
len Speicher hat jeweils nur die zugeordnete Rechenres-
source Zugriff. Werden Daten aus einem Speicher ben¨tigt,
o
der zu einer anderen Rechenressource lokal ist, so werden
Programmiermechanismen, wie z. B. Kommunikation uber ¨
ein Verbindungsnetzwerk, eingesetzt. Clustersysteme, aber
auch viele Parallelrechner geh¨ren in diese Klasse.
o
Rechner mit gemeinsamem Speicher bestehen aus
mehreren Rechenressourcen und einem globalen oder ge-
meinsamen Speicher, auf den alle Rechenressourcen uber ¨
ein Verbindungsnetzwerk auf Daten zugreifen k¨nnen. Da-
o
durch kann jede Rechenressource die gesamten Daten des
2.3 Parallele Programmiermodelle 29
parallelen Programms zugreifen und verarbeiten. Server-
Architekturen und insbesondere Multicore-Prozessoren ar-
beiten mit einem physikalisch gemeinsamen Speicher.
Die durch die Hardware gegebene Organisation des
Speichers in verteilten und gemeinsamen Speicher kann f¨ r u
den Programmierer in Form privater oder gemeinsamer Va-
riable sichtbar und nutzbar sein. Es ist prinzipiell jedoch
mit Hilfe entsprechender Softwareunterst¨ tzung m¨glich,
u o
das Programmieren mit gemeinsamen Variablen (shared va-
riables) auch auf physikalisch verteiltem Speicher bereitzu-
stellen. Ebenso kann die Programmierung mit verteiltem
Adressraum und Kommunikation auf einem physikalisch
gemeinsamen Speicher durch zus¨tzliche Software m¨glich
a o
sein. Dies ist nur ein Beispiel daf¨ r, dass die gegebene Hard-
u
ware nur ein Teil dessen ist, was dem Softwareentwickler als
Sicht auf ein paralleles System dient.
2.3 Parallele Programmiermodelle
Der Entwurf eines parallelen Programmes basiert immer
auch auf einer abstrakten Sicht auf das parallele System,
auf dem die parallele Software abgearbeitet werden soll.
Diese abstrakte Sicht wird paralleles Programmiermo-
dell genannt und setzt sich aus mehr als nur der gege-
benen parallelen Hardware zusammen: Ein paralleles Pro-
grammiermodell beschreibt ein paralleles Rechensystem aus
der Sicht, die sich dem Softwareentwickler durch System-
software wie Betriebssystem, parallele Programmiersprache
oder -bibliothek, Compiler und Laufzeitbibliothek bietet.
Entsprechend viele durchaus unterschiedliche parallele Pro-
grammiermodelle bieten sich dem Softwareentwickler an.
Folgende Kriterien erlauben jedoch eine systematische Her-
30 2 Konzepte paralleler Programmierung
angehensweise an diese Vielfalt der Programmiermodelle
[59]:
• Auf welcher Ebene eines Programmes sollen parallele
Programmteile ausgef¨ hrt werden? (z.B. Instruktions-
u
ebene, Anweisungsebene oder Prozedurebene)
• Sollen parallele Programmteile explizit angegeben wer-
den? (explizit oder implizit parallele Programme)
• In welcher Form werden parallele Programmteile ange-
geben? (z.B. als beim Start des Programmes erzeugte
Menge von Prozessen oder etwa Tasks, die dynamisch
erzeugt und zugeordnet werden)
• Wie erfolgt die Abarbeitung der parallelen Programm-
teile im Verh¨ltnis zueinander? (SIMD oder SPMD (Sin-
a
gle Program, Multiple Data); synchrone oder asynchro-
ne Berechnungen)
• Wie findet der Informationsaustausch zwischen paral-
lelen Programmteilen statt? (Kommunikation oder ge-
meinsame Variable)
• Welche Formen der Synchronisation k¨nnen genutzt
o
werden? (z.B. Barrier-Synchronisation oder Sperrme-
chanismen)
Ebenen der Parallelit¨t
a
Unabh¨ngige und damit parallel abarbeitbare Aufgaben
a
k¨nnen auf sehr unterschiedlichen Ebenen eines Programms
o
auftreten, wobei f¨ r die Parallelisierung eines Programmes
u
meist jeweils nur eine Ebene genutzt wird.
• Parallelit¨t auf Instruktionsebene kann ausgenutzt
a
werden, falls zwischen zwei Instruktionen keine Daten-
abh¨ngigkeit besteht. Diese Form der Parallelit¨t kann
a a
durch Compiler auf superskalaren Prozessoren einge-
setzt werden.
2.3 Parallele Programmiermodelle 31
• Bei der Parallelit¨t auf Anweisungsebene werden
a
mehrere Anweisungen auf denselben oder verschiede-
nen Daten parallel ausgef¨ hrt. Eine Form ist die Da-
u
tenparallelit¨t, bei der Datenstrukturen wie Felder in
a
Teilbereiche unterteilt werden, auf denen parallel zuein-
ander dieselben Operationen ausgef¨ hrt werden. Die-
u
se Arbeitsweise wird im SIMD Modell genutzt, in dem
in jedem Schritt die gleiche Anweisung auf evtl. unter-
schiedlichen Daten ausgef¨ hrt wird.
u
• Bei der Parallelit¨t auf Schleifenebene werden un-
a
terschiedliche Iterationen einer Schleifenanweisung par-
allel zueinander ausgef¨ hrt. Besondere Auspr¨gungen
u a
sind die forall und dopar Schleife. Bei der forall-
Schleife werden die Anweisungen im Schleifenrumpf par-
allel in Form von Vektoranweisungen nacheinander ab-
gearbeitet. Die dopar-Schleife f¨ hrt alle Anweisungen
u
einer Schleifeniteration unabh¨ngig vor den anderen
a
Schleifeniterationen aus. Je nach Datenabh¨ngigkeiten
a
zwischen den Schleifeniterationen kann es durch die Par-
allelit¨t auf Schleifenebene zu unterschiedlichen Ergeb-
a
nissen kommen als bei der sequentiellen Abarbeitung
der Schleifenr¨ mpfe. Als parallele Schleife wird eine
u
Schleife bezeichnet, deren Schleifenr¨mpfe keine Daten-
u
abh¨ngigkeit aufweisen und somit eine parallele Abar-
a
beitung zum gleichen Ergebnis f¨ hrt wie die sequentielle
u
Abarbeitung. Parallele Schleifen spielen bei Program-
mierumgebungen wie OpenMP eine wesentliche Rolle.
• Bei der Parallelit¨t auf Funktionsebene werden ge-
a
samte Funktionsaktivierungen eines Programms parallel
zueinander auf verschiedenen Prozessoren oder Prozes-
sorkernen ausgef¨ hrt. Bestehen zwischen parallel aus-
u
zuf¨ hrenden Funktionen Daten- oder Kontrollabh¨ngig-
u a
keiten, so ist eine Koordination zwischen den Funktio-
nen erforderlich. Dies erfolgt in Form von Kommunika-
32 2 Konzepte paralleler Programmierung
tion und Barrier-Anweisungen bei Modellen mit verteil-
tem Adressraum. Ein Beispiel ist die Programmierung
mit MPI (Message Passing Interface) [59, 61]. Bei Mo-
dellen mit gemeinsamem Adressraum ist Synchronisati-
on erforderlich; dies ist also f¨ r die Programmierung von
u
Multicore-Prozessoren notwendig und wird in Kapitel 3
vorgestellt.
Explizite oder implizite Parallelit¨t
a
Eine Voraussetzung f¨ r die parallele Abarbeitung auf ei-
u
nem Multicore-Prozessor ist das Vorhandensein mehrerer
Berechnungsstr¨me. Diese k¨nnen auf recht unterschiedli-
o o
che Art erzeugt werden [60].
Bei impliziter Parallelit¨t braucht der Programmie-
a
rer keine Angaben zur Parallelit¨t zu machen. Zwei unter-
a
schiedliche Vertreter impliziter Parallelit¨t sind paralleli-
a
sierende Compiler oder funktionale Programmiersprachen.
Parallelisierende Compiler erzeugen aus einem gegebenen
sequentiellen Programm ein paralleles Programm und nut-
zen dabei Abh¨ngigkeitsanalysen, um unabh¨ngige Berech-
a a
nungen zu ermitteln. Dies ist in den meisten F¨llen eine
a
komplexe Aufgabe und die Erfolge parallelisierender Com-
piler sind entsprechend begrenzt [55, 66, 5, 2]. Programme
in einer funktionalen Programmiersprache bestehen aus ei-
ner Reihe von Funktionsdefinitionen und einem Ausdruck,
dessen Auswertung das Programmergebnis liefert. Das Po-
tential f¨ r Parallelit¨t liegt in der parallelen Auswertung
u a
der Argumente von Funktionen, da funktionale Program-
me keine Seiteneffekte haben und sich die Argumente somit
nicht beeinflussen k¨nnen [33, 64, 8]. Implizite Parallelit¨t
o a
wird teilweise auch von neuen Sprachen wie Fortress einge-
setzt, vgl. Abschnitt 7.1.
2.3 Parallele Programmiermodelle 33
Explizite Parallelit¨t mit impliziter Zerlegung liegt
a
vor, wenn der Programmierer zwar angibt, wo im Pro-
gramm Potential f¨ r eine parallele Bearbeitung vorliegt,
u
etwa eine parallele Schleife, die explizite Kodierung in Thre-
ads oder Prozesse aber nicht vornehmen muss. Viele paral-
lele FORTRAN-Erweiterungen nutzen dieses Prinzip.
Bei einer expliziten Zerlegung muss der Program-
mierer zus¨tzlich angeben, welche Tasks es f¨ r die parallele
a u
Abarbeitung geben soll, ohne aber eine Zuordnung an Pro-
zesse oder explizite Kommunikation formulieren zu m¨ ssen.
u
Ein Beispiel ist BSP [65]. Die explizite Zuordnung der Tasks
an Prozesse wird in Koordinationssprachen wie Linda [12]
zus¨tzlich angegeben.
a
Bei Programmiermodellen mit expliziter Kommuni-
kation und Synchronisation muss der Programmierer
alle Details der parallelen Abarbeitung angeben. Hierzu
geh¨rt das Message-Passing-Programmiermodell mit MPI,
o
aber auch Programmierumgebungen zur Benutzung von
Threads, wie Pthreads, das in Kap. 4 vorgestellt wird.
Angabe paralleler Programmteile
Sollen vom Programmierer die parallelen Programmteile ex-
plizit angegeben werden, so kann dies in ganz unterschied-
licher Form erfolgen. Bei der Angabe von Teilaufgaben in
Form von Tasks werden diese implizit Prozessoren oder
Kernen zugeordnet. Bei vollkommen explizit paralleler Pro-
grammierung sind die Programmierung von Threads oder
von Prozessen die weit verbreiteten Formen.
Thread-Programmierung: Ein Thread ist eine Fol-
ge von Anweisungen, die parallel zu anderen Anweisungsfol-
gen, also Threads, abgearbeitet werden k¨nnen. Die Thre-
o
ads eines einzelnen Programmes besitzen f¨ r die Abarbei-
u
tung jeweils eigene Ressourcen, wie Programmz¨hler, Sta-
a
34 2 Konzepte paralleler Programmierung
tusinformationen des Prozessors oder einen Stack f¨ r loka-
u
le Daten, nutzen jedoch einen gemeinsamen Datenspeicher.
Damit ist das Thread-Modell f¨ r die Programmierung von
u
Multicore-Prozessoren geeignet.
Message-Passing-Programmierung: Die Message-
Passing-Programmierung nutzt Prozesse, die Programm-
teile bezeichnen, die jeweils auf einem separaten physika-
lischen oder logischen Prozessor abgearbeitet werden und
somit jeweils einen privaten Adressraum besitzen.
Abarbeitung paralleler Programmteile
Die Abarbeitung paralleler Programmteile kann synchron
erfolgen, indem die Anweisungen paralleler Threads oder
Prozesse jeweils gleichzeitig abgearbeitet werden, wie et-
wa im SIMD-Modell, oder asynchron, also unabh¨ngig von-
a
einander bis eine explizite Synchronisation erfolgt, wie et-
wa im SPMD-Modell. Diese Festlegung der Abarbeitung
wird meist vom Programmiermodell der benutzten Pro-
grammierumgebung vorgegeben. Dar¨ ber hinaus gibt es ei-
u
ne Reihe von Programmiermustern, in denen parallele Pro-
grammteile angeordnet werden, z.B. Pipelining, Master-
Worker oder Produzenten-Konsumenten-Modell, und die
vom Softwareentwickler explizit ausgew¨hlt werden.
a
Informationsaustausch
Ein wesentliches Merkmal f¨ r den Informationsaustausch
u
ist die Organisation des Adressraums. Bei einem verteilten
Adressraum werden Daten durch Kommunikation ausge-
tauscht. Dies kann explizit im Programm angegeben sein,
aber auch durch einen Compiler oder das Laufzeitsystem
erzeugt werden. Bei einem gemeinsamen Adressraum kann
2.4 Parallele Leistungsmaße 35
der Informationsaustausch einfach uber gemeinsame Varia-
¨
ble in diesem Adressraum geschehen, auf die lesend oder
schreibend zugegriffen werden kann. Hierdurch kann es je-
doch auch zu Konflikten oder unerw¨ nschten Ergebnis-
u
sen kommen, wenn dies unkoordiniert erfolgt. Die Koor-
dination von parallelen Programmteilen spielt also eine
wichtige Rolle bei der Programmierung eines gemeinsa-
men Adressraums und ist daher ein wesentlicher Bestand-
teil der Thread-Programmierung und der Programmierung
von Multicore-Prozessoren.
Formen der Synchronisation
Synchronisation gibt es in Form von Barrier-Synchronisa-
tion, die bewirkt, dass alle beteiligten Threads oder Pro-
zesse aufeinander warten, und im Sinne der Koordination
von Threads. Letzteres hat insbesondere mit der Vermei-
dung von Konflikten beim Zugriff auf einen gemeinsamen
Adressraum zu tun und setzt Sperrmechanismen und be-
dingtes Warten ein.
2.4 Parallele Leistungsmaße
Ein wesentliches Kriterium zur Bewertung eines parallelen
Programms ist dessen Laufzeit. Die parallele Laufzeit
Tp (n) eines Programmes ist die Zeit zwischen dem Start
der Abarbeitung des parallelen Programmes und der Been-
digung der Abarbeitung aller beteiligten Prozessoren. Die
parallele Laufzeit wird meist in Abh¨ngigkeit von der An-
a
zahl p der zur Ausf¨ hrung benutzten Prozessoren und ei-
u
ner Problemgr¨ße n angegeben, die z.B. durch die Gr¨ße
o o
der Eingabe gegeben ist. F¨ r Multicore-Prozessoren mit ge-
u
meinsamem Adressraum setzt sich die Laufzeit eines paral-
lelen Programmes zusammen aus:
36 2 Konzepte paralleler Programmierung
• der Zeit f¨ r die Durchf¨ hrung von Berechnungen durch
u u
die Prozessorkerne,
• der Zeit f¨ r die Synchronisation beim Zugriff auf ge-
u
meinsame Daten,
• den Wartezeiten, die z.B. wegen ungleicher Verteilung
der Last oder an Synchronisationspunkten entstehen.
Kosten: Die Kosten eines parallelen Programmes, h¨ufig a
auch Arbeit oder Prozessor-Zeit-Produkt genannt, be-
r¨ cksichtigen die Zeit, die alle an der Ausf¨ hrung beteilig-
u u
ten Prozessoren insgesamt zur Abarbeitung des Program-
mes verwenden. Die Kosten Cp (n) eines parallelen Pro-
gramms sind definiert als
Cp (n) = Tp (n) · p
und sind damit ein Maß f¨ r die von allen Prozessoren
u
durchgef¨ hrte Arbeit. Ein paralleles Programm heißt kos-
u
tenoptimal, wenn Cp (n) = T ∗ (n) gilt, d.h. wenn insge-
samt genauso viele Operationen ausgef¨ hrt werden wie vom
u
schnellsten sequentiellen Verfahren, das Laufzeit T ∗ (n) hat.
Speedup: Zur Laufzeitanalyse paralleler Programme
ist insbesondere ein Vergleich mit einer sequentiellen Im-
plementierung von Interesse, um den Nutzen des Einsatzes
der Parallelverarbeitung absch¨tzen zu k¨nnen. F¨ r einen
a o u
solchen Vergleich wird oft der Speedup-Begriff als Maß f¨ r u
den relativen Geschwindigkeitsgewinn herangezogen. Der
Speedup Sp (n) eines parallelen Programmes mit Laufzeit
Tp (n) ist definiert als
T ∗ (n)
Sp (n) = ,
Tp (n)
wobei p die Anzahl der Prozessoren zur L¨sung des Pro-
o
blems der Gr¨ße n bezeichnet. Der Speedup einer paral-
o
lelen Implementierung gibt also den relativen Geschwin-
digkeitsvorteil an, der gegen¨ ber der besten sequentiellen
u
2.4 Parallele Leistungsmaße 37
Implementierung durch den Einsatz von Parallelverarbei-
tung auf p Prozessoren entsteht. Theoretisch gilt immer
Sp (n) ≤ p. Durch Cacheeffekte kann in der Praxis auch
der Fall Sp (n) > p (superlinearer Speedup) auftreten.
Effizienz: Alternativ zum Speedup kann der Begriff der
Effizienz eines parallelen Programmes benutzt werden, der
ein Maß f¨ r den Anteil der Laufzeit ist, den ein Prozessor
u
f¨ r Berechnungen ben¨tigt, die auch im sequentiellen Pro-
u o
gramm vorhanden sind. Die Effizienz Ep (n) eines parallelen
Programms ist definiert als
T ∗ (n) T ∗ (n)
Sp (n)
Ep (n) = = =
Cp (n) p p · Tp (n)
wobei T ∗ (n) die Laufzeit des besten sequentiellen Algorith-
mus und Tp (n) die parallele Laufzeit ist. Liegt kein super-
linearer Speedup vor, gilt Ep (n) ≤ 1. Der ideale Speedup
Sp (n) = p entspricht einer Effizienz Ep (n) = 1.
Amdahlsches Gesetz: Die m¨gliche Verringerung von
o
Laufzeiten durch eine Parallelisierung sind oft begrenzt. So
stellt etwa die Anzahl der Prozessoren die theoretisch obe-
re Schranke des Speedups dar. Weitere Begrenzungen lie-
gen im zu parallelisierenden Algorithmus selber begr¨ ndet,
u
der neben parallelisierbaren Anteilen auch durch Daten-
abh¨ngigkeiten bedingte, inh¨rent sequentielle Anteile ent-
a a
halten kann. Der Effekt von Programmteilen, die sequentiell
ausgef¨ hrt werden m¨ ssen, auf den erreichbaren Speedup
u u
wird durch das Amdahlsche Gesetz quantitativ erfasst
[6]:
Wenn bei einer parallelen Implementierung ein (kon-
stanter) Bruchteil f (0 ≤ f ≤ 1) sequentiell ausgef¨ hrt u
werden muss, setzt sich die Laufzeit der parallelen Imple-
mentierung aus der Laufzeit f · T ∗ (n) des sequentiellen
Teils und der Laufzeit des parallelen Teils, die mindestens
(1 − f )/p · T ∗ (n) betr¨gt, zusammen. F¨ r den erreichbaren
a u
38 2 Konzepte paralleler Programmierung
Speedup gilt damit
T ∗ (n) 1 1
Sp (n) = ≤.
=
∗ (n) + 1−f T ∗ (n) 1−f f
f ·T f+ p
p
Bei dieser Berechnung wird der beste sequentielle Al-
gorithmus verwendet und es wurde angenommen, dass sich
der parallel ausf¨ hrbare Teil perfekt parallelisieren l¨sst.
u a
Durch ein einfaches Beispiel sieht man, dass nicht paral-
lelisierbare Berechnungsteile einen großen Einfluss auf den
erreichbaren Speedup haben: Wenn 20% eines Programmes
sequentiell abgearbeitet werden m¨ ssen, betr¨gt nach Aus-
u a
sage des Amdahlschen Gesetzes der maximal erreichbare
Speedup 5, egal wie viele Prozessoren eingesetzt werden.
Nicht parallelisierbare Teile m¨ ssen insbesondere bei einer
u
großen Anzahl von Prozessoren besonders beachtet werden.
Skalierbarkeit: Das Verhalten der Leistung eines par-
allelen Programmes bei steigender Prozessoranzahl wird
durch die Skalierbarkeit erfasst. Die Skalierbarkeit eines
parallelen Programmes auf einem gegebenen Parallelrech-
ner ist ein Maß f¨ r die Eigenschaft, einen Leistungsgewinn
u
proportional zur Anzahl p der verwendeten Prozessoren
zu erreichen. Der Begriff der Skalierbarkeit wird in unter-
schiedlicher Weise pr¨zisiert, z.B. durch Einbeziehung der
a
Problemgr¨ße n. Eine h¨ufig beobachtete Eigenschaft par-
o a
alleler Algorithmen ist es, dass f¨ r festes n und steigendes
u
p eine S¨ttigung des Speedups eintritt, dass aber f¨ r fes-
a u
tes p und steigende Problemgr¨ße n ein h¨herer Speedup
o o
erzielt wird. In diesem Sinne bedeutet Skalierbarkeit, dass
die Effizienz eines parallelen Programmes bei gleichzeiti-
gem Ansteigen von Prozessoranzahl p und Problemgr¨ße n o
konstant bleibt.
3
Thread-Programmierung
Die Programmierung von Multicore-Prozessoren ist eng mit
der parallelen Programmierung eines gemeinsamen Adress-
raumes und der Thread-Programmierung verbunden. Meh-
rere Berechnungsstr¨me desselben Programms k¨nnen par-
o o
allel zueinander bearbeitet werden und greifen dabei auf
Variablen des gemeinsamen Speichers zu. Diese Berech-
nungsstr¨me werden als Threads bezeichnet. Die Pro-
o
grammierung mit Threads ist ein seit vielen Jahren bekann-
tes Programmierkonzept [9] und kann vom Softwareent-
wickler durch verschiedene Programmierumgebungen oder
-bibliotheken wie Pthreads, Java-Threads, OpenMP oder
Win32 f¨ r Multithreading-Programme genutzt werden.
u
3.1 Threads und Prozesse
Die Abarbeitung von Threads h¨ngt eng mit der Abar-
a
beitung von Prozessen zusammen, so dass beide zun¨chst
a
nochmal genauer definiert und voneinander abgegrenzt wer-
den.
40 3 Thread-Programmierung
Prozesse
Ein Prozess ist ein sich in Ausf¨ hrung befindendes Pro-
u
gramm und umfasst neben dem ausf¨ hrbaren Programmco-
u
de alle Informationen, die zur Ausf¨ hrung des Programms
u
erforderlich sind. Dazu geh¨ren die Daten des Programms
o
auf dem Laufzeitstack oder Heap, die zum Ausf¨ hrungszeit-
u
punkt aktuellen Registerinhalte und der aktuelle Wert des
Programmz¨hlers, der die n¨chste auszuf¨ hrende Instruk-
a a u
tion des Prozesses angibt. Jeder Prozess hat also seinen
eigenen Adressraum. Alle diese Informationen ¨ndern sich
a
w¨hrend der Ausf¨ hrung des Prozesses dynamisch. Wird
a u
die Rechenressource einem anderen Prozess zugeordnet, so
muss der Zustand des suspendierten Prozesses gespeichert
werden, damit die Ausf¨ hrung dieses Prozesses zu einem
u
sp¨teren Zeitpunkt mit genau diesem Zustand fortgesetzt
a
werden kann. Dies wird als Kontextwechsel bezeichnet
und ist je nach Hardwareunterst¨ tzung relativ aufwendig
u
[54]. Prozesse werden bei Multitasking im Zeitscheibenver-
fahren von der Rechenressource abgearbeitet; es handelt
sich also um Nebenl¨ufigkeit und keine Gleichzeitigkeit. Bei
a
Multiprozessor-Systemen ist eine tats¨chliche Parallelit¨t
a a
m¨glich.
o
Beim Erzeugen eines Prozesses muss dieser die zu sei-
ner Ausf¨ hrung erforderlichen Daten erhalten. Im UNIX-
u
Betriebssystem kann ein Prozess P1 mit Hilfe einer fork-
Anweisung einen neuen Prozess P2 erzeugen. Der neue
Kindprozess P2 ist eine identische Kopie des Elternpro-
zesses P1 zum Zeitpunkt des fork-Aufrufes. Dies bedeu-
tet, dass der Kindprozess auf einer Kopie des Adressrau-
mes des Elternprozesses arbeitet und das gleiche Programm
wie der Elternprozess ausf¨ hrt, und zwar ab der der fork-
u
Anweisung folgenden Anweisung. Der Kindprozess hat je-
doch eine eigene Prozessnummer und kann in Abh¨ngigkeit
a
3.1 Threads und Prozesse 41
von dieser Prozessnummer andere Anweisungen als der El-
ternprozess ausf¨ hren, vgl. [46]. Da jeder Prozess einen ei-
u
genen Adressraum hat, ist die Erzeugung und Verwaltung
von Prozessen je nach Gr¨ße des Adressraumes relativ zeit-
o
aufwendig. Weiter kann bei h¨ufiger Kommunikation der
a
Austausch von Daten (¨ ber Sockets) einen nicht unerheb-
u
lichen Anteil der Ausf¨ hrungszeit ausmachen.
u
Threads
Das Threadmodell ist eine Erweiterung des Prozessmodells.
Jeder Prozess besteht anstatt nur aus einem aus mehreren
unabh¨ngigen Berechnungsstr¨men, die w¨hrend der Ab-
a o a
arbeitung des Prozesses durch ein Schedulingverfahren der
Rechenressource zugeteilt werden. Die Berechnungsstr¨me o
eines Prozesses werden als Threads bezeichnet. Das Wort
Thread wurde gew¨hlt, um anzudeuten, dass eine zusam-
a
menh¨ngende, evtl. sehr lange Folge von Instruktionen ab-
a
gearbeitet wird.
Ein wesentliches Merkmal von Threads besteht dar-
in, dass die verschiedenen Threads eines Prozesses sich
den Adressraum des Prozesses teilen, also einen gemein-
samen Adressraum haben. Wenn ein Thread einen Wert im
Adressraum ablegt, kann daher ein anderer Thread des glei-
chen Prozesses diesen unmittelbar darauf lesen. Damit ist
der Informationsaustausch zwischen Threads im Vergleich
zur Kommunikation zwischen Prozessen uber Sockets sehr
¨
schnell. Da die Threads eines Prozesses sich einen Adress-
raum teilen, braucht auch die Erzeugung von Threads we-
sentlich weniger Zeit als die Erzeugung von Prozessen. Das
Kopieren des Adressraumes, das z.B. in UNIX beim Er-
zeugen von Prozessen mit einer fork-Anweisung notwendig
ist, entf¨llt. Das Arbeiten mit mehreren Threads innerhalb
a
eines Prozesses ist somit wesentlich flexibler als das Arbei-
42 3 Thread-Programmierung
ten mit kooperierenden Prozessen, bietet aber die gleichen
Vorteile. Insbesondere ist es m¨glich, die Threads eines Pro-
o
zesses auf verschiedenen Prozessoren oder Prozessorkernen
parallel auszuf¨ hren.
u
Threads k¨nnen auf Benutzerebene als Benutzer-Thre-
o
ads oder auf Betriebssystemebene als Betriebssystem-
Threads implementiert werden. Threads auf Benutzerebe-
ne werden durch eine Thread-Bibliothek ohne Beteiligung
des Betriebssystems verwaltet. Ein Wechsel des ausgef¨ hr-
u
ten Threads kann damit ohne Beteiligung des Betriebssys-
tems erfolgen und ist daher in der Regel wesentlich schneller
als der Wechsel bei Betriebssystem-Threads.
Bibliotheks− BP Betriebssystem−
T
Scheduler Scheduler
T BP
T P
BP
T
P
BP
Prozess 1
P
BP
Bibliotheks−
T
P
BP
Scheduler
T
Prozessoren
BP
T
Prozess n
Betriebssystem−
Prozesse
N:1-Abbildung – Thread-Verwaltung oh-
Abbildung 3.1.
ne Betriebssystem-Threads. Der Scheduler der Thread-Bibliothek
w¨hlt den auszuf¨hrenden Thread T des Benutzerprozesses aus.
a u
Jedem Benutzerprozess ist ein Betriebssystemprozss BP zugeord-
net. Der Betriebssystem-Scheduler w¨hlt die zu einem bestimmten
a
Zeitpunkt auszuf¨hrenden Betriebssystemprozesse aus und bildet
u
diese auf die Prozessoren P ab.
3.1 Threads und Prozesse 43
Der Nachteil von Threads auf Benutzerebene liegt darin,
dass das Betriebssystem keine Kenntnis von den Threads
hat und nur gesamte Prozesse verwaltet. Wenn ein Thread
eines Prozesses das Betriebssystem aufruft, um z.B. eine
I/O-Operation durchzuf¨ hren, wird der CPU-Scheduler des
u
Betriebssystems den gesamten Prozess suspendieren und
die Rechenressource einem anderen Prozess zuteilen, da das
Betriebssystem nicht weiß, dass innerhalb des Prozesses zu
einem anderen Thread umgeschaltet werden kann. Dies gilt
f¨ r Betriebssystem-Threads nicht, da das Betriebssystem
u
die Threads direkt verwaltet.
BT Betriebssystem−
T
Scheduler
T BT
T P
BT
T
P
BT
Prozess 1
P
BT
P
T BT
T BT Prozessoren
T
Betriebssystem−
Prozess n
Threads
Abbildung 3.2. 1:1-Abbildung – Thread-Verwaltung mit
Betriebssystem-Threads. Jeder Benutzer-Thread T wird eindeutig
einem Betriebssystem-Thread BT zugeordnet.
44 3 Thread-Programmierung
Bibliotheks− BT Betriebssystem−
T
Scheduler Scheduler
T BT
T P
BT
T
P
BT
Prozess 1
P
BT
P
T BT
T
Prozessoren
BT
T Bibliotheks−
Scheduler
Prozess n Betriebssystem−
Threads
Abbildung 3.3. N:M-Abbildung – Thread-Verwaltung mit
Betriebssystem-Threads und zweistufigem Scheduling. Benutzer-
Threads T verschiedener Prozesse werden einer Menge von
Betriebssystem-Threads BT zugeordnet (N:M-Abbildung).
Ausf¨hrungsmodelle f¨r Threads
u u
Wird eine Thread-Verwaltung durch das Betriebssystem
nicht unterst¨ tzt, so ist die Thread-Bibliothek f¨ r das Sche-
u u
duling der Threads verantwortlich. Alle Benutzer-Threads
eines Prozesses werden vom Bibliotheks-Scheduler auf einen
Betriebssystem-Prozess abgebildet, was N:1-Abbildung
genannt wird, siehe Abb. 3.1. Stellt das Betriebssystem eine
Thread-Verwaltung zur Verf¨ gung, so gibt es f¨ r die Ab-
u u
bildung von Benutzer-Threads auf Betriebssystem-Threads
zwei M¨glichkeiten: Die erste ist die 1:1-Abbildung, die
o
f¨ r jeden Benutzer-Thread einen Betriebssystem-Thread
u
erzeugt, siehe Abb. 3.2. Der Betriebssystem-Scheduler w¨hlta
den jeweils auszuf¨ hrenden Betriebssystem-Thread aus und
u
verwaltet bei Mehr-Prozessor-Systemen die Ausf¨ hrung der
u
Betriebssystem-Threads auf den verschiedenen Prozesso-
3.1 Threads und Prozesse 45
ren. Die zweite M¨glichkeit ist die N:M-Abbildung, die
o
ein zweistufiges Schedulingverfahren anwendet, siehe Abb.
3.3. Der Scheduler der Thread-Bibliothek ordnet die ver-
schiedenen Threads der verschiedenen Prozesse einer vor-
gegebenen Menge von Betriebssystem-Threads zu, wobei
ein Benutzer-Thread zu verschiedenen Zeitpunkten auf ver-
schiedene Betriebssystem-Threads abgebildet werden kann.
Zust¨nde eines Threads
a
Ob ein Thread gerade von einem Prozessor oder Prozes-
sorkern abgearbeitet wird, h¨ngt nicht nur vom Scheduler,
a
sondern auch von seinem Zustand ab. Threads k¨nnen sich
o
in verschiedenen Zust¨nden befinden:
a
• neu erzeugt beendet
neu
• lauff¨hig
a
• laufend Start Ende
• wartend Unterbrechung
• beendet lauf−
laufend
fähig
Zuteilung
Abbildung 3.4 ver- Au ng
ru
fw
ie
ec
anschaulicht die Zu- ck
ke
lo
n B
stands¨ berg¨nge. Die
u a wartend
¨
Uberg¨nge zwischen
a
lauff¨hig und laufend Abbildung 3.4. Zust¨nde eines Thre-
a
a
ads.
werden vom Schedu-
ler bestimmt. Blockierung bzw. Warten kann durch I/O-
Operationen, aber auch durch die Koordination zwischen
den Threads eintreten.
Sichtbarkeit von Daten
Die Threads eines Prozesses teilen sich einen gemeinsamen
Adressraum, d.h. die globalen Variablen und alle dynamisch
46 3 Thread-Programmierung
}
Stackbereich
Stackdaten
für Hauptthread
}
Stackdaten Stackbereich
für Thread 1
}
Stackdaten Stackbereich
für Thread 2
...
Heapdaten
globale Daten
Programmcode
Adresse 0
Abbildung 3.5. Laufzeitverwaltung f¨r ein Programm mit meh-
u
reren Threads.
erzeugten Datenobjekte sind von allen erzeugten Threads
des Prozesses zugreifbar. F¨ r jeden Thread wird jedoch ein
u
eigener Laufzeitstack gehalten, auf dem die von dem Thread
aufgerufenen Funktionen mit ihren lokalen Variablen ver-
waltet werden, siehe Abb. 3.5. Die auf dem Laufzeitstack
verwalteten, also statisch deklarierten Daten, sind lokale
Daten des zugeh¨rigen Threads und k¨nnen von anderen
o o
Threads nicht zugegriffen werden. Da der Laufzeitstack ei-
nes Threads nur so lange existiert, wie der Thread selbst,
kann ein Thread einen m¨glichen R¨ ckgabewert an einen
o u
anderen Thread nicht uber seinen Laufzeitstack ubergeben
¨ ¨
und es m¨ ssen andere Techniken verwendet werden.
u
3.2 Synchronisations-Mechanismen
Wichtig bei der Thread-Programmierung ist die Koordina-
tion der Threads, die durch Synchronisations-Mecha-
nismen erreicht wird. Die Koordination der Threads ei-
nes Multithreading-Programms wird vom Softwareentwick-
3.2 Synchronisations-Mechanismen 47
ler eingesetzt, um eine gew¨ nschte Ausf¨ hrungsreihenfolge
u u
der beteiligten Threads zu erreichen oder um den Zugriff
auf gemeinsame Daten zu gestalten. Die Koordination des
Zugriffs auf den gemeinsamen Speicher dient der Vermei-
dung von unerw¨ nschten Effekten beim gleichzeitigen Zu-
u
griff auf dieselbe Variable. Dies trifft f¨ r Multithreading-
u
Programme auf einer Rechenressource mit nebenl¨ufigera
Abarbeitung im Zeitscheibenverfahren zu, aber auch auf
eine tats¨chlich parallele Abarbeitung auf mehreren Re-
a
chenressourcen. Da die Threads eines Prozesses im We-
sentlichen uber gemeinsame Daten kooperieren, bewirkt ei-
¨
ne bestimmte Ausf¨ hrungsreihenfolge einen speziellen Zu-
u
stand des gemeinsamen Speichers, also eine bestimmte Be-
legung der gemeinsamen Variablen mit Werten, die f¨ r alle
u
Threads sichtbar sind. Die Effekte der Kooperation und Ko-
ordination k¨nnen jedoch bei Nebenl¨ufigkeit anders sein
o a
als bei Parallelit¨t.
a
Koordination der Berechnungsstr¨me
o
Eine Barrier-Synchronisation bewirkt, dass alle betei-
ligten Threads aufeinander warten und keiner der Threads
eine nach der Synchronisationsanweisung stehende Anwei-
sung ausf¨ hrt, bevor alle anderen Threads diese erreicht ha-
u
ben. Dadurch erscheint den Threads der gemeinsame Spei-
cher aller beteiligten Threads in dem Zustand, der durch
die Abarbeitung aller Anweisungen aller Threads vor der
Barrier-Synchronisation erreicht wird.
Zeitkritische Abl¨ufe
a
Das lesende und schreibende Zugreifen verschiedener Thre-
ads auf dieselbe gemeinsame Variable kann zu sogenann-
ten zeitkritischen Abl¨ufen f¨ hren. Dies bedeutet, dass
a u
48 3 Thread-Programmierung
das Ergebnis der Ausf¨ hrung eines Programmst¨ cks durch
u u
mehrere Threads von der relativen Ausf¨ hrungsgeschwin-
u
digkeit der Threads zueinander abh¨ngt: Wenn das Pro-
a
grammst¨ ck zuerst von Thread T1 und dann von Thread
u
T2 ausgef¨ hrt wird, kann ein anderes Ergebnis berechnet
u
werden als wenn dieses Programmst¨ ck zuerst von Thread
u
T2 und dann von Thread T1 ausgef¨hrt wird. Das Auf-
u
treten von zeitkritischen Abl¨ufen ist meist unerw¨ nscht,
a u
da die relative Ausf¨ hrungsreihenfolge von vielen Fakto-
u
ren abh¨ngen kann (z.B. der Ausf¨ hrungsgeschwindigkeit
a u
der Prozessoren, dem Auftreten von Interrupts oder der
Belegung der Eingabedaten), die vom Programmierer nur
bedingt zu beeinflussen sind. Es entsteht ein nichtdeter-
ministisches Verhalten, da f¨ r die Ausf¨ hrungsreihenfol-
u u
ge und das Ergebnis verschiedene M¨glichkeiten eintreten
o
k¨nnen, ohne dass dies vorhergesagt werden kann.
o
Kritischer Bereich
Ein Programmst¨ ck, in dem Zugriffe auf gemeinsame Varia-
u
blen vorkommen, die auch konkurrierend von anderen Thre-
ads zugegriffen werden k¨nnen, heißt kritischer Bereich.
o
Eine fehlerfreie Abarbeitung kann dadurch gew¨hrleistet
a
werden, dass durch einen wechselseitigen Ausschluss
(oder mutual exclusion) jeweils nur ein Thread auf Varia-
blen zugreifen kann, die in einem kritischen Bereich liegen.
Programmiermodelle f¨ r einen gemeinsamen Adressraum
u
stellen Operationen und Mechanismen zur Sicherstellung
des wechselseitigen Ausschlusses zur Verf¨ gung, mit de-
u
nen erreicht werden kann, dass zu jedem Zeitpunkt nur ein
Thread auf eine gemeinsame Variable zugreift. Die grund-
legenden Mechanismen, die angeboten werden, sind Sperr-
mechanismus und Bedingungs-Synchronisation.
3.2 Synchronisations-Mechanismen 49
Sperrmechanismus
Zur Vermeidung des Auftretens zeitkritischer Abl¨ufe mit
a
Hilfe eines Sperrmechanismus wird eine Sperrvariable
(oder Mutex-Variable von mutual exclusion) s eines spezi-
ell vorgegebenen Typs verwendet, die mit den Funktionen
lock(s) und unlock(s) angesprochen wird. Vor Betreten des
kritischen Bereichs f¨ hrt der Thread lock(s) zur Belegung
u
der Sperrvariable s aus; nach Verlassen des Programmseg-
ments wird unlock(s) zur Freigabe der Sperrvariable aufge-
rufen. Nur wenn jeder Prozessor diese Vereinbarung einh¨lt,
a
werden zeitkritische Abl¨ufe vermieden.
a
Der Aufruf lock(s) hat den Effekt, dass der aufrufende
Thread T1 nur dann das dieser Sperrvariablen zugeordnete
Programmsegment ausf¨ hren kann, wenn gerade kein an-
u
derer Thread diese Sperrvariable belegt hat. Hat jedoch ein
anderer Thread T2 zuvor lock(s) ausgef¨ hrt und die Sperr-
u
variable s noch nicht mit unlock(s) wieder freigegeben, so
wird Thread T1 so lange blockiert, bis Thread T2 unlock(s)
aufruft. Der Aufruf unlock(s) bewirkt neben der Freiga-
be der Sperrvariablen auch das Aufwecken anderer bzgl.
der Sperrvariablen s blockierter Threads. Die Verwendung
eines Sperrmechanismus kann also zu einer Sequentiali-
sierung f¨ hren, da Threads durch ihn nur nacheinander
u
auf eine gemeinsame Variable zugreifen k¨nnen. Sperrme-
o
chanismen sind in Laufzeitbibliotheken wie Pthreads, Java-
Threads oder OpenMP auf leicht unterschiedliche Art rea-
lisiert.
Bedingungs-Synchronisation
Bei einer Bedingungs-Synchronisation wird ein Thread
T1 so lange blockiert bis eine bestimmte Bedingung ein-
getreten ist. Das Aufwecken des blockierten Threads kann
50 3 Thread-Programmierung
nur durch einen anderen Thread T2 erfolgen. Dies geschieht
sinnvollerweise nachdem durch die Ausf¨ hrung von Thread
u
T2 diese Bedingung eingetreten ist. Da jedoch mehrere
Threads auf dem gemeinsamen Adressraum arbeiten und
dadurch zwischenzeitlich wieder Ver¨nderungen der Bedin-
a
gung erfolgt sein k¨nnten, muss die Bedingung durch den
o
aufgeweckten Thread T1 nochmal uberpr¨ ft werden. Die
u
¨
Bedingungs-Synchronisation wird durch Bedingungsva-
riablen realisiert; zum Schutz vor zeitkritischen Abl¨ufen
a
wird zus¨tzlich ein Sperrmechanismus verwendet.
a
Semaphor-Mechanismus
Ein weiterer Mechanismus zur Realisierung eines wechsel-
seitigen Ausschlusses ist der Semaphor [19]. Ein Sema-
phor ist eine Struktur, die eine Integervariable s enth¨lt, auf
a
die zwei atomare Operationen P (s) und V (s) angewendet
werden k¨nnen. Ein bin¨rer Semaphor kann nur die Werte
o a
0 und 1 annehmen. Werden weitere Werte angenommen,
spricht man von einem z¨hlenden Semaphor. Die Operati-
a
on P (s) (oder wait(s)) wartet bis der Wert von s gr¨ßer o
als 0 ist, dekrementiert den Wert von s anschließend um 1
und erlaubt dann die weitere Ausf¨ hrung der nachfolgenden
u
Berechnungen. Die Operation V (s) (oder signal(s)) inkre-
mentiert den Wert von s um 1. Der genaue Mechanismus
der Verwendung von P und V zum Schutz eines kritischen
Bereiches ist nicht streng festgelegt. Eine ubliche Form ist:
¨
wait(s)
kritischer Bereich
signal(s)
Verschiedene Threads f¨ hren die Operationen P und V auf
u
s aus und koordinieren so ihren Zugriff auf kritische Berei-
3.3 Effiziente und korrekte Thread-Programme 51
che. F¨ hrt etwa Threads T1 die Operation wait(s) aus um
u
danach seinen kritischen Bereich zu bearbeiten, so wird je-
der andere Threads T2 beim Aufruf von wait(s) am Eintritt
in seinen kritischen Bereich so lange gehindert, bis T1 die
Operation signal(s) ausf¨hrt.
u
Monitor
Ein abstrakteres Konzept stellt der Monitor dar [31]. Ein
Monitor ist ein Sprachkonstrukt, das Daten und Operatio-
nen, die auf diese Daten zugreifen, in einer Struktur zusam-
menfasst. Auf die Daten eines Monitors kann nur durch
dessen Monitoroperationen zugegriffen werden. Da zu je-
dem Zeitpunkt die Ausf¨ hrung nur einer Monitoroperation
u
erlaubt ist, wird der wechselseitige Ausschluss bzgl. der Da-
ten des Monitors automatisch sichergestellt.
3.3 Effiziente und korrekte
Thread-Programme
Je nach Applikation kann durch Synchronisation eine enge
und komplizierte Verzahnung von Threads entstehen, was
zu Problemen wie Leistungseinbußen durch Sequentialisie-
rung oder sogar zu Deadlocks f¨ hren kann.
u
Anzahl der Threads und Sequentialisierung
Die Laufzeit eines parallelen Programms kann je nach Ent-
wurf und Umsetzung sehr verschieden sein. Um ein effizi-
entes paralleles Programm zu erhalten, sollte schon beim
Entwurf darauf geachtet werden, dass
• eine geeignete Anzahl von Threads genutzt wird und
52 3 Thread-Programmierung
• Sequentialisierungen nach M¨glichkeit vermieden wer-
o
den.
Die Erzeugung von Threads bewirkt Parallelit¨t, so dass
a
eine hinreichend große Anzahl von Threads im parallelen
Programm vorhanden sein sollte, um alle Prozessorkerne
mit Arbeit zu versorgen und so die verf¨ gbaren parallelen
u
Ressourcen gut auszunutzen. Andererseits sollte die Anzahl
der Threads auch nicht zu groß werden, da erstens der An-
teil der Arbeit f¨ r einen einzelnen Thread im Vergleich zum
u
Overhead f¨ r Erzeugung, Verwaltung und Terminierung des
u
Threads zu gering werden kann, und da zweitens viele Hard-
wareressourcen (vor allem Caches) von den Prozessorkernen
geteilt werden und es so zu Leistungsverlusten bei der Lese-
/Schreib-Bandbreite kommen kann.
Aufgrund der notwendigen Kooperationen zwischen den
Threads kann die vorgegebene Parallelit¨t nicht immer
a
ausgenutzt werden, da zur Vermeidung von zeitkritischen
Abl¨ufen Synchronisations-Mechanismen eingesetzt werden
a
m¨ ssen. Bei h¨ufiger Synchronisation kann es jedoch dazu
u a
kommen, dass immer nur einer oder wenige der Threads
aktiv sind, w¨hrend alle anderen auf Grund der Synchroni-
a
sation warten, so dass eine Nacheinanderausf¨ hrung, also
u
Sequentialisierung, auftritt.
Deadlock
Die Nutzung von Sperr- und anderen Synchronisations-
Mechanismen hilft Nichtdeterminismus und zeitkritische
Abl¨ufe zu vermeiden. Die Nutzung von Sperren kann je-
a
doch zu einem Deadlock (Verklemmung) im Anwendungs-
programm f¨ hren, wenn die Abarbeitung in einen Zustand
u
kommt, in dem jeder Thread auf ein Ereignis wartet, das
nur von einem anderen Thread ausgel¨st werden kann, der
o
aber auch vergeblich auf ein Ereignis wartet.
3.3 Effiziente und korrekte Thread-Programme 53
Allgemein ist ein Deadlock f¨ r eine Menge von Akti-
u
vit¨ten dann gegeben, wenn jede der Aktivit¨ten auf ein
a a
Ereignis wartet, das nur durch eine der anderen Aktivit¨ten
a
hervorgerufen werden kann, so dass ein Zyklus des gegen-
seitigen Aufeinanderwartens entsteht. Ein Beispiel f¨ r einen
u
Deadlock ist folgende Situation:
• Thread T1 versucht zuerst Sperre s1 und dann Sperre s2
zu belegen; nach Sperrung von s1 wird er unterbrochen;
• Thread T2 versucht zuerst Sperre s2 und dann Sperre s1
zu belegen; nach Sperrung von s2 wird er unterbrochen;
Nachdem T1 Sperre s1 und T2 Sperre s2 belegt hat, warten
beide Threads auf die Freigabe der fehlenden Sperre durch
den jeweils anderen Thread, die nicht eintreten kann.
Die Verwendung von Sperrmechanismen sollte also gut
geplant sein, um diesen Fall etwa durch eine spezielle Rei-
henfolge der Sperrbelegung zu vermeiden, vgl. auch [59].
Speicherzugriffszeiten und Cacheeffekte
Speicherzugriffszeiten k¨nnen einen hohen Anteil an der
o
parallelen Laufzeit eines Programms haben. Die Speicher-
zugriffe eines Programms f¨ hren zum Transfer von Daten
u
zwischen Speicher und den Caches der Prozessorkerne. Die-
ser Datentransfer wird durch Lese- und Schreiboperationen
der Kerne ausgel¨st und kann nicht direkt vom Program-
o
mierer gesteuert werden.
Zwischen Datenzugriffen verschiedener Prozessorkerne
k¨nnen verschiedene Abh¨ngigkeiten auftreten: Lese-Lese-
o a
Abh¨ngigkeiten, Lese-Schreib-Abh¨ngigkeiten und Schreib-
a a
Schreib-Abh¨ngigkeiten. Lesen zwei Prozessorkerne diesel-
a
ben Daten, so kann dies evtl. ohne Speicherzugriff aus den
jeweiligen Caches erfolgen. Die beiden anderen Abh¨ngig-
a
keiten l¨sen Speicherzugriffe aus, da die Daten zwischen
o
54 3 Thread-Programmierung
den Prozessorkernen ausgetauscht werden m¨ ssen. Die An-
u
zahl der Speicherzugriffe kann reduziert werden, indem der
Zugriff der Prozessorkerne auf gemeinsame Daten so gestal-
tet wird, dass die Kerne auf verschiedene Daten zugreifen.
Dies sollte bereits beim Entwurf des parallelen Programms
ber¨ cksichtigt werden.
u
False Sharing, bei dem zwei verschiedene Threads auf
verschiedene Daten zugreifen, die jedoch in derselben Ca-
chezeile liegen, l¨st jedoch ebenfalls Speicheroperationen
o
aus. False Sharing kann vom Programmierer nur schwer
beeinflusst werden, da auch eine weit auseinandergezogene
Abspeicherung von Daten nicht immer zum Erfolg f¨hrt.
u
3.4 Parallele Programmiermuster
Parallele oder verteilte Programme bestehen aus einer An-
sammlung von Tasks, die in Form von Threads auf verschie-
denen Rechenressourcen ausgef¨ hrt werden. Zur Struktu-
u
rierung der Programme k¨nnen parallele Muster verwendet
o
werden, die sich in der parallelen Programmierung als sinn-
voll herausgestellt haben, siehe z.B. [56] oder [45]. Diese
Muster geben eine spezielle Koordinationsstruktur der be-
teiligten Threads vor.
Erzeugung von Threads
Die Erzeugung von Threads kann statisch oder dynamisch
erfolgen. Im statischen Fall wird meist eine feste Anzahl
von Threads zu Beginn der Abarbeitung des parallelen Pro-
gramms erzeugt, die w¨hrend der gesamten Abarbeitung
a
existieren und erst am Ende des Gesamtprogramms beendet
werden. Alternativ k¨nnen Threads zu jedem Zeitpunkt der
o
Programmabarbeitung (statisch oder dynamisch) erzeugt
3.4 Parallele Programmiermuster 55
und beendet werden. Zu Beginn der Abarbeitung ist meist
nur ein einziger Thread aktiv, der das Hauptprogramm ab-
arbeitet.
Fork-Join
Das Fork-Join-Konstrukt ist eines der einfachsten Kon-
zepte zur Erzeugung von Threads oder Prozessen [15], das
von der Programmierung mit Prozessen herr¨ hrt, aber als
u
Muster auch f¨ r Threads anwendbar ist. Ein bereits existie-
u
render Thread T1 spaltet mit einem Fork-Aufruf einen wei-
teren Thread T2 ab. Bei einem zugeordneten Join-Aufruf
des Threads T1 wartet dieser auf die Beendigung des Thre-
ads T2 .
Das Fork-Join-Konzept kann explizit als Sprachkon-
strukt oder als Bibliotheksaufruf zur Verf¨ gung stehen
u
und wird meist in der Programmierung mit gemeinsamem
Adressraum verwendet. Die Spawn- und Exit-Operationen
der Message-Passing-Programmierung, also der Program-
mierung mit verteiltem Adressraum, bewirken im Wesent-
lichen dieselben Aktionen wie die Fork-Join-Operationen.
Obwohl das Fork-Join-Konzept sehr einfach ist, erlaubt
es durch verschachtelte Aufrufe eine beliebige Struktur
paralleler Aktivit¨t. Spezielle Programmiersprachen und
a
Programmierumgebungen haben oft eine spezifische Aus-
pr¨gung der beschriebenen Erzeugung von Threads.
a
Parbegin-Parend
Eine strukturierte Variante der Thread-Erzeugung wird
durch das gleichzeitige Erzeugen und Beenden mehrerer
Threads erreicht. Dazu wird das Parbegin-Parend-Kon-
strukt bereitgestellt, das manchmal auch mit dem Namen
Cobegin-Coend bezeichnet wird. Zwischen Parbegin und
56 3 Thread-Programmierung
Parend werden Anweisungen angegeben, die auch Funkti-
onsaufrufe beinhalten k¨nnen und die Threads zur Ausf¨ h-
o u
rung zugeordnet werden k¨nnen. Erreicht der ausf¨ hrende
o u
Thread den Parbegin-Befehl, so werden die von Parbegin-
Parend umgebenen Anweisungen separaten Threads zur
Ausf¨ hrung zugeordnet. Der Programmtext nach dem Par-
u
end-Befehl wird erst ausgef¨ hrt, wenn alle so gestarteten
u
Threads beendet sind. Die Threads innerhalb des Parbegin-
Parend-Konstrukts k¨nnen gleichen oder verschiedenen Pro-
o
grammtext haben. Ob und wie die Threads tats¨chlich par-
a
allel ausgef¨ hrt werden, h¨ngt von der zur Verf¨ gung ste-
u a u
henden Hardware und der Implementierung des Konstrukts
ab. Die Anzahl und Art der zu erzeugenden Threads steht
meist statisch fest. Auch f¨ r dieses Konstrukt haben spe-
u
zielle parallele Sprachen oder Umgebungen ihre spezifische
Syntax und Auspr¨gung, wie z.B. in Form von parallelen
a
Bereichen (parallel sections), vgl. auch OpenMP.
SPMD und SIMD
Im SIMD- (Single Instruction, Multiple Data) und SPMD-
Programmiermodell (Single Program, Multiple Data) wird
zu Programmbeginn eine feste Anzahl von Threads gestar-
tet. Alle Threads f¨ hren dasselbe Programm aus, das sie auf
u
verschiedene Daten anwenden. Durch Kontrollanweisungen
innerhalb des Programmtextes kann jeder Thread verschie-
dene Programmteile ausw¨hlen und ausf¨ hren. Im SIMD-
a u
Ansatz werden die einzelnen Instruktionen synchron abge-
arbeitet, d.h. die verschiedenen Threads arbeiten dieselbe
Instruktion gleichzeitig ab. Der Ansatz wird auch h¨ufig als
a
Datenparallelit¨t im engeren Sinne bezeichnet. Im SPMD-
a
Ansatz k¨nnen die Threads asynchron arbeiten, d.h. zu
o
einem Zeitpunkt k¨nnen verschiedene Threads verschie-
o
dene Programmstellen bearbeiten. Dieser Effekt tritt ent-
3.4 Parallele Programmiermuster 57
weder durch unterschiedliche Ausf¨ hrungsgeschwindigkei-
u
ten oder eine Verz¨gerung des Kontrollflusses in Abh¨ngig-
o a
keit von lokalen Daten auf. Der SPMD-Ansatz ist z.Zt. ei-
ner der popul¨rsten Ans¨tze der parallelen Programmie-
a a
rung, insbesondere in der Programmierung mit verteiltem
Adressraum mit MPI. Besonders geeignet ist die SPMD-
Programmierung f¨ r Anwendungsalgorithmen, die auf Fel-
u
dern arbeiten und bei denen eine Zerlegung der Felder die
Grundlage einer Parallelisierung ist.
Master-Slave oder Master-Worker
Bei diesem Ansatz kontrolliert ein einzelner Thread die
gesamte Arbeit eines Programms. Dieser Master-Thread
entspricht oft dem Hauptprogramm des Anwendungspro-
gramms. Der Master-Prozess erzeugt mehrere, meist gleich-
artige Worker- oder Slave-Threads, die die eigentlichen
Berechnungen ausf¨ hren, siehe Abb. 3.6. Diese Worker-
u
Threads k¨nnen statisch oder dynamisch erzeugt werden.
o
Die Zuteilung von Arbeit an die Worker-Threads kann
durch den Master-Thread erfolgen. Die Worker-Threads
k¨nnen aber auch eigenst¨ndig Arbeit allokieren. In diesem
o a
Fall ist der Master-Thread nur f¨ r alle ubrigen Koordina-
u ¨
tionsaufgaben zust¨ndig, wie etwa Initialisierung, Zeitmes-
a
sung oder Ausgabe.
Client-Server-Modell
Programmierstrukturierungen nach dem Client-Server-Prin-
zip ¨hneln dem MPMD-Modell (Multiple Program, Mul-
a
tiple Data). Es stammt aus dem verteilten Rechnen, wo
mehrere Client-Rechner mit einem als Server dienenden
Mainframe verbunden sind, der etwa Anfragen an eine
Datenbank bedient. Parallelit¨t kann auf der Server-Seite
a
58 3 Thread-Programmierung
Master Server An
fra
ge
g
ge
An
r un
St
f ra
tw
Anfrage
Antwort
Steuerung
rt
or
eu
An
o
ue
t
tw
eru
Ste
An
Client 3
ng
Client 1
Client 2
Slave 1 Slave 3
Slave 2
Abbildung 3.6. Veranschaulichung Master-Slave-Modell (links)
und Client-Server-Modell (rechts).
auftreten, indem mehrere Client-Anfragen verschiedener
Clients nebenl¨ufig oder parallel zueinander beantwortet
a
werden. Eine parallele Programmstrukturierung nach dem
Client-Server-Prinzip nutzt mehrere Client-Threads, die An-
fragen an einen Server-Thread stellen, siehe Abb. 3.6. Nach
Erledigung der Anfrage durch den Server-Thread geht die
Antwort an den jeweiligen Client-Thread zur¨ ck. Das Client-
u
Server-Prinzip kann auch weiter gefasst werden, indem et-
wa mehrere Server-Threads vorhanden sind oder die Thre-
ads des Programmes die Rolle von Clients und von Servern
ubernehmen und sowohl Anfragen stellen als auch beant-
¨
worten k¨nnen.
o
Pipelining
Der Pipelining-Ansatz beschreibt eine spezielle Form der
Zusammenarbeit verschiedener Threads, bei der Daten zwi-
schen den Threads weitergereicht werden. Die beteiligten
Threads T1 , . . . , Tp sind dazu logisch in einer vorgegebe-
nen Reihenfolge angeordnet. Thread Ti erh¨lt die Ausgabe
a
von Thread Ti−1 als Eingabe und produziert eine Ausgabe,
die dem n¨chsten Thread Ti+1 , i = 2, . . . , p − 1 als Einga-
a
be dient; Thread T1 erh¨lt die Eingabe von anderen Pro-
a
grammteilen und Tp gibt seine Ausgabe an wiederum ande-
3.4 Parallele Programmiermuster 59
re Progammteile weiter. Jeder Thread verarbeitet also einen
Strom von Eingaben in einer sequentiellen Reihenfolge und
produziert einen Strom von Ausgaben. Somit k¨nnen die
o
Threads durch Anwendung des Pipeline-Prinzips trotz der
Datenabh¨ngigkeiten parallel zueinander ausgef¨ hrt wer-
a u
den.
Pipelining kann als spezielle Form einer funktionalen
Zerlegung betrachtet werden, bei der die Threads Funktio-
nen eines Anwendungsalgorithmus bearbeiten, die durch ih-
re Datenabh¨ngigkeiten nicht nacheinander ausgef¨ hrt wer-
a u
den, sondern auf die beschriebene Weise gleichzeitig abgear-
beitet werden k¨nnen. Das Pipelining-Konzept kann prin-
o
zipiell mit gemeinsamem Adressraum oder mit verteiltem
Adressraum realisiert werden.
Taskpools
Ein Taskpool ist eine Datenstruktur, in der die noch abzu-
arbeitenden Programmteile (Tasks) eines Programms etwa
in Form von Funktionen abgelegt sind. F¨ r die Abarbeitung
u
der Tasks wird eine feste Anzahl von Threads verwendet,
die zu Beginn des Programms vom Haupt-Thread erzeugt
werden und bis zum Ende des Programms existieren. F¨ r u
die Threads ist der Taskpool eine gemeinsame Datenstruk-
tur, auf die sie zugreifen k¨nnen, um die dort abgelegten
o
Tasks zu entnehmen und anschließend abzuarbeiten, sie-
he Abb. 3.7. W¨hrend der Abarbeitung einer Task kann
a
ein Thread neue Tasks erzeugen und diese in den Taskpool
einf¨ gen. Der Zugriff auf den Taskpool muss synchronisiert
u
werden. Die Abarbeitung des parallelen Programms ist be-
endet, wenn der Taskpool leer ist und jeder Thread seine
Tasks abgearbeitet hat. Der Vorteil dieses Abarbeitungs-
schemas besteht darin, dass auf der einen Seite nur ein fes-
te Anzahl von Threads erzeugt werden muss und daher der
60 3 Thread-Programmierung
Aufwand zur Thread-Erzeugung unabh¨ngig von der Pro-
a
blemgr¨ße und relativ gering ist, dass aber auf der anderen
o
Seite Tasks dynamisch erzeugt werden k¨nnen und so auch
o
adaptive und irregul¨re Anwendungen effizient abgearbei-
a
tet werden k¨nnen.
o
1
Th Produzent 1
ad
Konsument 1
Ta −
sk e re
ab sk−
re
e
Ta lag
hm
ad
A
Th
T la
bl
a
ab sk− me
en as ge
tn
3
age
En
tn k− Ta ah
ah Task− tn
m en Daten−
e
pool Konsument 2
Ta
k− e Produzent 2 puffer
ab sk
s
Ta lag k− e en Ta lag −
ab Tas hm tn sk e
ge
Th
ah −
4
a
En
a
tn bl
m
ad
re
tn
A
en e
a
ad
re
hm
Th
Produzent 3 Konsument 3
e
2
Abbildung 3.7. Veranschaulichung eines Taskpool-Konzepts
(links) und eines Produzenten-Konsumenten-Modells (rechts).
Produzenten-Konsumenten-Modell
Das Produzenten-Konsumenten-Modell nutzt Produzenten-
Threads und Konsumenten-Threads, wobei die Produzen-
ten-Threads Daten erzeugen, die von Konsumenten-Threads
¨
als Eingabe genutzt werden. F¨ r die Ubergabe der Daten
u
wird eine gemeinsame Datenstruktur vorgegebener L¨nge a
als Puffer benutzt, auf die beide Threadtypen schreibend
bzw. lesend zugreifen. Die Produzenten-Threads legen die
von ihnen erzeugten Eintr¨ge in den Puffer ab, die Kon-
a
sumenten-Threads entnehmen Eintr¨ge aus dem Puffer und
a
verarbeiten diese weiter, siehe Abb. 3.7. Die Produzenten
k¨nnen nur Eintr¨ge im Puffer ablegen, wenn dieser nicht
o a
voll ist. Entsprechend k¨nnen die Konsumenten nur Ein-
o
tr¨ge entnehmen, wenn der Puffer nicht leer ist. Zum kor-
a
rekten Ablauf ist f¨ r den Zugriff auf die Pufferdatenstruktur
u
eine Synchronisation der zugreifenden Threads erforderlich.
3.5 Parallele Programmierumgebungen 61
3.5 Parallele Programmierumgebungen
F¨ r die parallele Programmierung steht eine Vielzahl von
u
Umgebungen zur Verf¨ gung. Die verbreitetsten sind:
u
Posix Threads: Posix Threads (auch Pthreads genannt)
ist eine portable Thread-Bibliothek, die f¨ r viele Betriebs-
u
systeme nutzbar ist. Mittlerweile ist Pthreads die Standard-
Schnittstelle f¨ r Linux und wird auch f¨ r Unix-Plattformen
u u
h¨ufig genutzt. F¨ r Windows ist eine Open-Source-Version
a u
pthreads-win32 verf¨ gbar. Die Programmiersprache ist C.
u
Kapitel 4 stellt Pthreads detaillierter vor.
Win32/MFC Thread API: Das Win32/MFC API bietet
dem Softwareentwickler eine C/C++-Umgebung zur Ent-
wicklung von Windows-Anwendungen. Es werden Mecha-
nismen zur Erzeugung und Verwaltung von Threads zur
Verf¨ gung gestellt sowie Kommunikations- und Synchroni-
u
sations-Mechanismen. Wir verweisen u.a. auf [3] f¨ r eine
u
genauere Beschreibung.
Threading API f¨ r Microsoft.NET: Das .NET-Frame-
u
work bietet umfangreiche Unterst¨ tzung f¨ r die Program-
u u
mierung mit Threads f¨ r die Sprachen C++, Visual Basic,
u
.NET, JScript.NET oder C#. Das Laufzeitsystem wird als
Common Language Runtime (CLR) bezeichnet; CLR ar-
beitet mit einer Zwischencodedarstellung ¨hnlich zu Java
a
Bytecode, siehe [3] f¨ r eine detailliertere Beschreibung.
u
Java-Threads: Die Programmiersprache Java unterst¨ tzt u
die Erzeugung, Verwaltung und Synchronisation von Thre-
ads auf Sprachebene bzw. durch Bereitstellung spezieller
Klassen und Methoden. Das Paket java.util.concurrent
(ab Java 1.5) stellt eine Vielzahl zus¨tzlicher Synchronisa-
a
tions-Mechanismen zur Verf¨ gung. Kapitel 5 enth¨lt eine
u a
detailliertere Einf¨ hrung.
u
OpenMP: OpenMP ist ein API zur Formulierung por-
tierbarer Multithreading-Programme, das Fortran, C und
62 3 Thread-Programmierung
C++ unterst¨ tzt. Das Programmiermodell wird durch eine
u
plattformunabh¨ngige Menge von Compiler-Pragmas und
a
-Direktiven, Funktionsaufrufen und Umgebungsvariablen
realisiert, die die Erstellung paralleler Programme verein-
¨
fachen sollen. Kapitel 6 gibt einen genaueren Uberblick.
Message Passing Interface (MPI): MPI [26, 59] wur-
de als Standard f¨ r die Kommunikation zwischen Prozessen
u
mit jeweils separatem Adressraum definiert und stellt eine
Vielzahl von Kommunikationsoperationen zur Verf¨ gung,
u
die sowohl Einzeltransfers (mit jeweils zwei Kommunika-
tionspartnern) als auch globale Kommunikationsoperatio-
nen wie Broadcast- oder Reduktionsoperationen umfassen.
Sprachanbindungen wurden f¨ r C, C++ und Fortran de-
u
finiert, es gibt aber auch MPI-Implementierungen f¨ r Ja-
u
va. Obwohl MPI f¨ r einen verteilten Adressraum entworfen
u
wurde, kann es im Prinzip auch f¨ r die Programmierung
u
von Multicore-Prozessoren mit gemeinsamem Adressraum
verwendet werden. Dazu wird auf jedem Prozessorkern ein
separater Prozess mit privaten Daten gestartet. Der Daten-
bzw. Informationsaustausch zwischen den Prozessen erfolgt
mit MPI-Kommunikationsoperationen, Zugriffe auf den ge-
meinsamen Speicher und damit auch die damit verbunde-
nen Synchronisationsoperationen entfallen. Im Vergleich zu
Threads stellt dies ein v¨llig anderes Programmiermodell
o
dar, das je nach Anwendungsprogramm aber durchaus zu
einer ¨hnlichen Prozessorauslastung wie die Verwendung ei-
a
nes Threadmodells f¨ hren kann.
u
Das MPI-Modell ist insbesondere f¨ r solche Program-
u
me geeignet, in denen jeder Berechnungsstrom auf einen
ihm zuzuordnenden Datenbereich zugreift und relativ selten
Daten anderer Datenbereiche braucht. Wir gehen im Fol-
genden nicht n¨her auf MPI ein und verweisen auf [59, 26]
a
f¨ r eine detaillierte Beschreibung.
u
4
Programmierung mit Pthreads
Posix Threads (auch Pthreads genannt) ist ein Standard
zur Programmierung im Threadmodell mit der Program-
miersprache C. Dieser Abschnitt f¨ hrt in den Pthreads-
u
Standard kurz ein; vollst¨ndigere Behandlungen sind in
a
[11, 35, 42, 49, 56] zu finden.
Die von einer Pthreads-Bibliothek verwendeten Daten-
typen, Schnittstellendefinitionen und Makros sind ublicher-
¨
weise in der Headerdatei <pthread.h> abgelegt, die somit
in jedes Pthreads-Programm eingebunden werden muss. Al-
le Pthreads-Funktionen liefern den Wert 0 zur¨ ck, wenn
u
sie fehlerfrei durchgef¨ hrt werden konnten. Wenn bei der
u
Durchf¨ hrung ein Fehler aufgetreten ist, wird ein Fehlerco-
u
de aus <error.h> zur¨ ckgegeben. Diese Headerdatei sollte
u
daher ebenfalls eingebunden werden.
4.1 Threaderzeugung und -verwaltung
Beim Start eines Pthreads-Programms ist ein Haupt-Thread
(main thread) aktiv, der die main()-Funktion des Pro-
64 4 Programmierung mit Pthreads
gramms ausf¨ hrt. Ein Thread ist in der Pthreads-Bibliothek
u
durch den Typ pthread t dargestellt. Der Haupt-Thread
kann weitere Threads erzeugen, indem jeweils die Funktion
int pthread create (pthread t *thread,
const pthread attr t *attr,
void *(*start routine)(void *),
void *arg)
aufgerufen wird. Das erste Argument ist ein Zeiger auf
ein Datenobjekt vom Typ pthread t. In diesem Argument
wird eine Identifikation des erzeugten Threads ablegt, die
auch als Thread-Name (thread identifier, TID) bezeichnet
wird und mit der dieser Thread in nachfolgenden Aufru-
fen von Pthreads-Funktionen angesprochen werden kann.
Das zweite Argument ist ein Zeiger auf ein Attributobjekt
vom Typ pthread attr t, mit dessen Hilfe das Verhalten
des Threads (wie z.B. Scheduling, Priorit¨ten, Gr¨ße des
a o
Laufzeitstacks) beeinflusst werden kann. Die Angabe von
NULL bewirkt, dass ein Thread mit den Default-Attributen
erzeugt wird. Sollen die Attribute abweichend gesetzt wer-
den, muss die Attributdatenstruktur vor dem Aufruf von
¨
pthread create() entsprechend besetzt werden. Ublicher-
weise reicht die Verwendung der Default-Attribute aus. Das
dritte Argument bezeichnet die Funktion start routine(),
die der Thread nach seiner Erzeugung ausf¨ hrt. Diese Funk-
u
tion hat ein einziges Argument vom Typ void * und lie-
fert einen Wert des gleichen Typs zur¨ ck. Das vierte Argu-
u
ment ist ein Zeiger auf das Argument, mit dem die Funktion
start routine() ausgef¨ hrt werden soll.
u
Um mehrere Argumente an die Startfunktion eines Thre-
ads zu ubergeben, m¨ ssen diese in eine Datenstruktur ge-
u
¨
packt werden, deren Adresse an die Funktion ubergeben
¨
wird. Sollen mehrere Threads die gleiche Funktion mit
unterschiedlichen Argumenten ausf¨ hren, so sollte jedem
u
4.1 Threaderzeugung und -verwaltung 65
Thread ein Zeiger auf eine separate Datenstruktur als Ar-
gument der Startfunktion mitgegeben werden, um zu ver-
meiden, dass Argumentwerte zu fr¨ h uberschrieben werden
u¨
oder dass verschiedene Threads ihre Argumentwerte kon-
kurrierend ver¨ndern.
a
Ein Thread wird beendet, indem er die auszuf¨ hrende
u
Startfunktion vollst¨ndig abarbeitet oder aber die Biblio-
a
theksfunktion
void pthread exit (void *valuep)
aufruft, wobei valuep den R¨ ckgabewert bezeichnet, der an
u
den aufrufenden Thread oder einen anderen Thread zur¨ ck- u
gegeben wird, wenn dieser mit pthread join() auf die Be-
endigung des Threads wartet. Wenn ein Thread seine Start-
funktion beendet, wird die Funktion pthread exit() im-
plizit aufgerufen, und der R¨ ckgabewert der Startfunktion
u
wird zur¨ ckgegeben. Da nach dem Aufruf von pthread -
u
exit() der aufgerufene Thread und damit auch der von ihm
verwendete Laufzeitstack nicht mehr existiert, sollte f¨ r den
u
R¨ ckgabewert valuep keine lokale Variable der Startfunk-
u
tion oder einer anderen Funktion verwendet werden. Diese
werden auf dem Laufzeitstack aufgehoben und k¨nnen nach
o
dessen Freigabe durch einen anderen Thread uberschrieben
¨
werden. Stattdessen sollte eine globale oder eine dynamisch
allokierte Variable verwendet werden.
Ein Thread kann auf die Beendigung eines anderen
Threads warten, indem er die Bibliotheksfunktion
int pthread join (pthread t thread, void **valuep)
aufruft, wobei thread die Identifikation des Threads an-
gibt, auf dessen Beendigung gewartet wird. Der aufrufende
Thread wird so lange blockiert, bis der angegebene Thread
beendet ist. Die Funktion pthread join bietet also eine
66 4 Programmierung mit Pthreads
M¨glichkeit der Synchronisation von Threads. Der R¨ckga-
o u
bewert des beendeten Threads thread wird dem wartenden
Thread in der Variable valuep zur¨ ckgeliefert.
u
Die Pthreads-Bibliothek legt f¨ r jeden erzeugten Thread
u
eine interne Datenstruktur an, die die f¨ r die Abarbeitung
u
des Threads notwendigen Informationen enth¨lt. Diese Da-
a
tenstruktur wird von der Bibliothek auch nach Beendigung
eines Threads aufgehoben, damit ein anderer Thread eine
pthread join()-Operation erfolgreich durchf¨ hren kann.
u
Durch den Aufruf von pthread join() wird auch die in-
terne Datenstruktur des angegebenen Threads freigegeben
und kann danach nicht mehr verwendet werden.
4.2 Koordination von Threads
Die Threads eines Prozesses teilen sich einen gemeinsamen
Adressraum und k¨nnen daher konkurrierend auf gemein-
o
same Variablen zugreifen. Um dabei zeitkritische Abl¨ufe
a
zu vermeiden, m¨ ssen die Zugriffe der beteiligten Thre-
u
ads koordiniert werden. Als wichtigste Hilfsmittel stellen
Pthreads-Bibliotheken Mutexvariablen und Bedingungsva-
riablen zur Verf¨ gung.
u
Eine Mutexvariable bezeichnet eine Datenstruk-
tur des vorgegebenen Typs pthread mutex t, die dazu
verwendet werden kann, den wechselseitigen Ausschluss
beim Zugriff auf gemeinsame Variablen sicherzustellen. Ei-
ne Mutexvariable kann zwei Zust¨nde annehmen: gesperrt
a
(locked) und ungesperrt (unlocked). Um einen wechselseiti-
gen Ausschluss beim Zugriff auf gemeinsame Datenstruktu-
ren sicherzustellen, m¨ ssen die beteiligten Threads jeweils
u
folgendes Verhalten aufweisen: Bevor ein Thread eine Mani-
pulation der gemeinsamen Datenstruktur startet, sperrt er
die zugeh¨rige Mutexvariable mit einem speziellen Funkti-
o
4.2 Koordination von Threads 67
onsaufruf. Wenn ihm dies gelingt, ist er der Eigent¨mer der
u
Mutexvariable und er hat die Kontrolle uber sie. Nach Be-
¨
endigung der Manipulation der gemeinsamen Datenstruk-
tur gibt der Thread die Sperre der Mutexvariable wieder
frei.
Versucht ein Thread die Kontrolle uber eine von einem
¨
anderen Thread kontrollierte Mutexvariable zu erhalten,
wird er so lange blockiert, bis der andere Thread die Mu-
texvariable wieder freigegeben hat. Die Thread-Bibliothek
stellt also sicher, dass jeweils nur ein Thread die Kon-
trolle uber eine Mutexvariable hat. Wenn das beschrie-
¨
bene Verhalten beim Zugriff auf eine Datenstruktur ein-
gehalten wird, wird dadurch eine konkurrierende Manipu-
lation dieser Datenstruktur ausgeschlossen. Sobald jedoch
ein Thread die Datenstruktur manipuliert ohne vorher die
Kontrolle uber die Mutexvariable erhalten zu haben, ist ein
¨
wechselseitiger Ausschluss nicht mehr garantiert.
Die Zuordnung zwischen einer Mutexvariablen und der
ihr zugeordneten Datenstruktur erfolgt implizit dadurch,
dass die Zugriffe auf die Datenstruktur durch Sperren bzw.
Freigabe der Mutexvariablen gesch¨ tzt werden; eine expli-
u
zite Zuordnung existiert nicht. Die Lesbarkeit eines Pro-
gramms kann jedoch dadurch erleichtert werden, dass die
Datenstruktur und die f¨ r deren Kontrolle verwendete Mu-
u
texvariable in einer gemeinsamen Struktur gespeichert wer-
den. Mutexvariablen k¨nnen wie alle anderen Variablen de-
o
klariert oder dynamisch erzeugt werden. Bevor eine Mu-
texvariable benutzt werden kann, muss sie durch Aufruf
der Funktion
int pthread mutex init (pthread mutex t *mutex,
const pthread mutexattr t *attr)
initialisiert werden. F¨ r attr = NULL wird eine Mutexva-
u
riable mit den Default-Eigenschaften zur Verf¨ gung ge-
u
68 4 Programmierung mit Pthreads
stellt. Eine statisch deklarierte Mutexvariable mutex kann
auch durch die Zuweisung
mutex = PTHREAD MUTEX INITIALIZER
mit den Default-Attributen initialisiert werden. Eine initia-
lisierte Mutexvariable kann durch Aufruf der Funktion
int pthread mutex destroy (pthread mutex t *mutex)
wieder zerst¨rt werden. Eine Mutexvariable sollte erst dann
o
zerst¨rt werden, wenn kein Thread mehr auf ihre Freigabe
o
wartet. Eine zerst¨rte Mutexvariable kann durch eine er-
o
neute Initialisierung weiterverwendet werden.
Ein Thread erh¨lt die Kontrolle uber eine Mutexvaria-
a ¨
ble, indem er diese durch Aufruf der Funktion
int pthread mutex lock (pthread mutex t *mutex)
sperrt. Wird die angegebene Mutexvariable mutex bereits
von einem anderen Thread kontrolliert, so wird der nun die
Funktion pthread mutex lock() aufrufende Thread blo-
ckiert, bis der momentane Eigent¨ mer die Mutexvariable
u
wieder freigibt. Wenn mehrere Threads versuchen, die Kon-
trolle uber eine Mutexvariable zu erhalten, werden die auf
¨
deren Freigabe wartenden Threads in einer Warteschlange
gehalten. Welcher der wartenden Threads nach der Freiga-
be der Mutexvariable zuerst die Kontrolle uber diese erh¨lt,
a
¨
kann von den Priorit¨ten der wartenden Threads und dem
a
verwendeten Scheduling-Verfahren abh¨ngen. Ein Thread
a
kann eine von ihm kontrollierte Mutexvariable mutex durch
Aufruf der Funktion
int pthread mutex unlock (pthread mutex t *mutex)
4.2 Koordination von Threads 69
wieder freigeben. Wartet zum Zeitpunkt des Aufrufs von
pthread mutex unlock() kein anderer Thread auf die Frei-
gabe der Mutexvariable, so hat diese nach dem Aufruf kei-
nen Eigent¨ mer mehr. Wenn andere Threads auf die Frei-
u
gabe der Mutexvariable warten, wird einer dieser Threads
aufgeweckt und Eigent¨ mer der Mutexvariablen.
u
In manchen Situationen ist es sinnvoll, dass ein Thread
feststellen kann, ob eine Mutexvariable von einem anderen
Thread kontrolliert wird, ohne dass er dadurch blockiert
wird. Dazu steht die Funktion
int pthread mutex trylock (pthread mutex t *mutex)
zur Verf¨ gung. Beim Aufruf dieser Funktion erh¨lt der
u a
aufrufende Thread die Kontrolle uber die Mutexvariable
¨
mutex, wenn diese frei ist. Wenn diese zur Zeit von ei-
nem anderen Thread gesperrt ist, liefert der Aufruf EBUSY
zur¨ ck; dies f¨ hrt aber nicht wie beim Aufruf von pthread -
u u
mutex lock() zu einer Blockierung des aufrufenden Thre-
ads. Daher kann der aufrufende Thread so lange versuchen,
die Kontrolle uber die Mutexvariable zu erhalten, bis er
¨
erfolgreich ist (spinlock).
Beim gleichzeitigen Sperren mehrerer Mutexvariablen
durch mehrere Threads besteht die Gefahr, dass Deadlocks
auftreten, siehe Kapitel 3. Das Auftreten von Deadlocks
kann durch Verwenden einer festen Sperr-Reihenfolge oder
das Verwenden einer Backoff-Strategie vermieden werden,
vgl. [11, 59].
Mutexvariablen werden in erster Linie dazu verwendet,
den wechselseitigen Ausschluss beim Zugriff auf globale Da-
tenstrukturen sicherzustellen. Ist der wechselseitige Aus-
schluss f¨ r eine gesamte Funktion sichergestellt, wird sie
u
als thread-sicher bezeichnet. Eine thread-sichere Funkti-
on kann also gleichzeitig von mehreren Threads aufgerufen
werden, ohne dass die beteiligten Threads zur Vermeidung
70 4 Programmierung mit Pthreads
von zeitkritischen Abl¨ufen zus¨tzliche Operationen beim
a a
Funktionsaufruf ausf¨ hren m¨ ssen.
u u
Im Prinzip k¨nnen Mutexvariablen jedoch auch dazu
o
verwendet werden, auf das Eintreten einer Bedingung zu
warten, die vom Zustand globaler Datenstrukturen abh¨ngt.
a
Dazu verwendet der zugreifende Thread eine oder mehrere
Mutexvariablen zum Schutz des Zugriffs auf die globalen
Daten und wertet die gew¨ nschte Bedingung von Zeit zu
u
Zeit aus, indem er mit Hilfe der Mutexvariablen gesch¨ tzt
u
auf die entsprechenden globalen Daten zugreift. Wenn die
Bedingung erf¨ llt ist, kann der Thread die beabsichtigte
u
Operation ausf¨ hren. Diese Vorgehensweise hat den Nach-
u
teil, dass der auf das Eintreten der Bedingung warten-
de Thread die Bedingung evtl. sehr oft auswerten muss,
bis diese erf¨ llt ist, und dabei CPU-Zeit verbraucht (ak-
u
tives Warten). Um diesen Nachteil zu beheben, stellt der
Pthreads-Standard Bedingungsvariablen zur Verf¨ gung.
u
4.3 Bedingungsvariablen
Eine Bedingungsvariable ist eine Datenstruktur, die es
einem Thread erlaubt, auf das Eintreten einer beliebigen
Bedingung zu warten. F¨ r Bedingungsvariablen wird ein
u
Signalmechanismus zur Verf¨ gung gestellt, der den warten-
u
den Thread w¨hrend der Wartezeit blockiert, so dass er
a
keine CPU-Zeit verbraucht, und wieder aufweckt, sobald
die angegebene Bedingung erf¨ llt ist. Um diesen Mecha-
u
nismus zu verwenden, muss der ausf¨ hrende Thread neben
u
der Bedingungsvariablen einen Bedingungsausdruck an-
geben, der die Bedingung bezeichnet, auf deren Erf¨ llung
u
der Thread wartet. Eine Mutexvariable wird verwendet,
um die Auswertung des Bedingungsausdrucks zu sch¨ tzen.
u
Letzteres ist notwendig, da der Bedingungsausdruck in der
4.3 Bedingungsvariablen 71
Regel auf globale Datenstrukturen zugreift, die von anderen
Threads konkurrierend ver¨ndert werden k¨nnen.
a o
Bedingungsvariablen haben den Typ pthread cond t.
Nach der Deklaration oder der dynamischen Erzeugung ei-
ner Bedingungsvariablen muss diese initialisiert werden, be-
vor sie verwendet werden kann. Dies kann dynamisch durch
Aufruf der Funktion
int pthread cond init (pthread cond t *cond,
const pthread condattr t *attr)
geschehen. Dabei ist cond ein Zeiger auf die Bedingungsva-
riable und attr ein Zeiger auf eine Attribut-Datenstruktur
f¨ r Bedingungsvariablen. F¨ r attr = NULL erfolgt eine In-
u u
itialisierung mit den Default-Attributen. Die Initialisierung
kann auch bei der Deklaration einer Bedingungsvariablen
durch Verwendung eines Makros erfolgen:
pthread cond t cond = PTHREAD COND INITIALIZER.
Eine mit pthread cond init() dynamisch initialisierte
Bedingungsvariable cond sollte durch Aufruf der Funktion
int pthread cond destroy (pthread cond t *cond)
zerst¨rt werden, wenn sie nicht mehr gebraucht wird, damit
o
das Laufzeitsystem die f¨ r die Bedingungsvariable abgeleg-
u
te Information freigeben kann. Statisch initialisierte Bedin-
gungsvariablen m¨ ssen nicht freigegeben werden.
u
Eine Bedingungsvariable muss eindeutig mit einer Mu-
texvariablen assoziiert sein. Alle Threads, die zur gleichen
Zeit auf die Bedingungsvariable warten, m¨ ssen die glei-
u
che Mutexvariable verwenden, d.h. f¨ r eine Bedingungs-
u
variable d¨ rfen von verschiedenen Threads nicht verschie-
u
dene Mutexvariablen verwendet werden. Eine Mutexvaria-
ble kann jedoch verschiedenen Bedingungsvariablen zuge-
ordnet werden. Nach dem Sperren der Mutexvariablen mit
72 4 Programmierung mit Pthreads
pthread mutex lock() kann ein Thread durch Aufruf der
Funktion
int pthread cond wait (pthread cond t *cond,
pthread mutex t *mutex)
auf das Eintreten einer Bedingung warten. Dabei bezeich-
net cond die Bedingungsvariable und mutex die assoziier-
te Mutexvariable. Eine Bedingungsvariable sollte nur mit
einer Bedingung assoziiert sein, da sonst die Gefahr von
Deadlocks oder zeitkritischen Abl¨ufen vorliegt [11]. Die
a
typische Verwendung hat folgendes Aussehen:
pthread mutex lock (&mutex);
while (!Bedingung)
pthread cond wait (&cond, &mutex);
pthread mutex unlock (&mutex);
Die Auswertung der Bedingung wird zusammen mit dem
Aufruf von pthread cond wait() unter dem Schutz der
Mutexvariablen mutex ausgef¨ hrt, um sicherzustellen, dass
u
die Bedingung sich zwischen ihrer Auswertung und dem
Aufruf von pthread cond wait() nicht durch Berechnun-
gen anderer Threads ver¨ndert. Daher muss durch das Pro-
a
gramm auch gew¨hrleistet sein, dass jeder andere Thread
a
eine Manipulation einer in den Bedingungen auftreten-
den gemeinsamen Variable mit der gleichen Mutexvariablen
sch¨ tzt.
u
• Wenn bei der Ausf¨ hrung des Programmsegments die
u
angegebene Bedingung erf¨ llt ist, wird die pthread -
u
cond wait()-Funktion nicht aufgerufen, und der aus-
f¨ hrende Thread arbeitet nach pthread mutex unlock()
u
das nachfolgende Programm weiter ab.
• Wenn dagegen die Bedingung nicht erf¨ llt ist, wird
u
pthread cond wait() aufgerufen mit dem Effekt, dass
4.3 Bedingungsvariablen 73
der ausf¨ hrende Thread T1 gleichzeitig die Kontrolle
u
uber die Mutexvariable freigibt und so lange bez¨ glich
u
¨
der Bedingungsvariable blockiert, bis er von einem an-
deren Thread T2 mit einer pthread cond signal()-
Anweisung aufgeweckt wird, siehe unten. Wird Thread
T1 durch diese Anweisung wieder aufgeweckt, versucht
er automatisch, die Kontrolle uber die Mutexvariable
¨
mutex zur¨ ckzuerhalten. Hat bereits ein anderer Thread
u
Kontrolle uber die Mutexvariable mutex, so wird der
¨
aufgeweckte Thread T1 unmittelbar nach dem Aufwe-
cken so lange bzgl. der Mutexvariable blockiert, bis er
diese sperren kann. Erst wenn der aufgeweckte Thread
die Mutexvariable erfolgreich gesperrt hat, kann er mit
der Abarbeitung seines Programms fortfahren, was zu-
n¨chst die erneute Abarbeitung der Bedingung ist.
a
Das Programm sollte sicherstellen, dass der blockier-
te Thread nur dann aufgeweckt wird, wenn die angegebe-
ne Bedingung erf¨ llt ist. Trotzdem ist es sinnvoll, die Be-
u
dingung nach dem Aufwecken noch einmal zu uberpr¨ fen, u
¨
da ein gleichzeitig aufgeweckter oder zeitgleich arbeitender
Thread, der die Kontrolle uber die Mutexvariable zuerst
¨
erh¨lt, die Bedingung oder in der Bedingung enthaltene ge-
a
meinsame Daten modifizieren kann und so die Bedingung
nicht mehr erf¨ llt ist.
u
Zum Aufwecken von bzgl. einer Bedingungsvariable war-
tenden Threads stehen die beiden folgenden Funktionen zur
Verf¨ gung:
u
int pthread cond signal (pthread cond t *cond)
int pthread cond broadcast (pthread cond t *cond)
Ein Aufruf von pthread cond signal() weckt einen bzgl.
der Bedingungsvariable cond wartenden Thread auf, wenn
die Bedingung erf¨ llt ist. Wartet kein Thread, so hat der
u
74 4 Programmierung mit Pthreads
Aufruf keinen Effekt. Warten mehrere Threads, wird ein
Thread anhand der Priorit¨ten der Threads und der ver-
a
wendeten Scheduling-Strategie ausgew¨hlt. Ein Aufruf der
a
Funktion pthread cond broadcast() weckt alle bzgl. der
Bedingungsvariablen cond wartenden Threads auf. Dabei
kann aber h¨chstens einer dieser Threads die Kontrol-
o
le uber die mit der Bedingungsvariablen assoziierten Mu-
¨
texvariable erhalten; alle anderen bleiben bzgl. der Mu-
texvariablen blockiert.
Als Variante von pthread cond wait() steht die Funk-
tion
int pthread cond timedwait (pthread cond t *cond,
pthread mutex t *mutex,
const struct timespec *time)
zur Verf¨ gung. Der Unterschied zu pthread cond wait()
u
besteht darin, dass die Blockierung bzgl. der Bedingungsva-
riable cond aufgehoben wird, wenn die in time angegebene
absolute Zeit abgelaufen ist. In diesem Fall wird die Fehler-
meldung ETIMEDOUT zur¨ ckgeliefert.
u
Die Datenstruktur vom Typ timespec ist definiert als
struct timespec {
time t tv sec;
long tv nsec;
}
wobei tv sec die Anzahl der Sekunden und tv nsec die
zus¨tzliche Anzahl von Nanosekunden der verwendeten Zeit-
a
scheiben angibt. Der Parameter time von pthread cond -
timedwait() gibt eine absolute Tageszeit und kein relatives
Zeitintervall an.
Eine typische Benutzung ist in Abbildung 4.1 angegeben.
In diesem Beispiel wartet der ausf¨ hrende Thread maxi-
u
mal zehn Sekunden auf das Eintreten der Bedingung. Zur
4.4 Erweiterter Sperrmechanismus 75
pthread mutex t m = PTHREAD MUTEX INITIALIZER;
pthread cond t c = PTHREAD COND INITIALIZER;
struct timespec time;
pthread mutex lock (&m);
time.tv sec = time (NULL) + 10;
time.tv nsec = 0;
while (!Bedingung)
if (pthread cond timedwait (&c, &m, &time)
== ETIMEDOUT)
timed out work();
pthread mutex unlock (&m);
Abbildung 4.1. Typische Verwendung von Bedingungsvariablen.
Besetzung von time.tv sec wird die Funktion time aus
<time.h> benutzt. (Der Aufruf time (NULL) gibt die ab-
solute Zeit in Sekunden zur¨ ck, die seit dem 1. Januar 1970
u
vergangen ist.) Wenn die Bedingung nach zehn Sekunden
noch nicht erf¨ llt ist, wird die Funktion timed out work()
u
ausgef¨ hrt, und die Bedingung wird erneut uberpr¨ft.
u u
¨
4.4 Erweiterter Sperrmechanismus
Bedingungsvariablen k¨nnen dazu verwendet werden, kom-
o
plexere Synchronisationsmechanismen zu implementieren.
Als Beispiel hierf¨ r betrachten wir im Folgenden einen
u
Lese/Schreib-Sperrmechanismus, der als Erweiterung
des von Mutexvariablen zur Verf¨ gung gestellten Sperrme-
u
chanismus aufgefasst werden kann. Wird eine gemeinsame
Datenstruktur von einer normalen Mutexvariable gesch¨ tzt,
u
so kann zu einem Zeitpunkt jeweils nur ein Thread die
gemeinsame Datenstruktur lesen bzw. auf die gemeinsa-
me Datenstruktur schreiben. Die Idee des Lese/Schreib-
76 4 Programmierung mit Pthreads
Sperrmechanismus besteht darin, dies dahingehend zu er-
weitern, dass zum gleichen Zeitpunkt eine beliebige Anzahl
von lesenden Threads zugelassen wird, ein Thread zum Be-
schreiben der Datenstruktur aber das ausschließliche Zu-
griffsrecht haben muss. Wir werden im Folgenden eine ein-
fache Variante eines solchen modifizierten Sperrmechanis-
mus beschreiben, vgl. auch [50]. F¨ r eine komplexere und
u
effizientere Implementierung verweisen wir auf [11, 35].
F¨ r die Implementierung des erweiterten Sperrmecha-
u
nismus werden RW-Sperrvariablen (read/write lock va-
riables) verwendet, die mit Hilfe einer Mutex- und einer
Bedingungsvariablen wie folgt definiert werden k¨nnen:
o
typedef struct rw lock {
int num r, num w;
pthread mutex t mutex;
pthread cond t cond;
} rw lock t;
Dabei gibt num r die Anzahl der momentan erteilten Lese-
berechtigungen und num w die Anzahl der momentan erteil-
ten Schreibberechtigungen an. Letztere hat h¨chstens den
o
Wert Eins. Die Mutexvariable soll diese Z¨hler der Lese-
a
und Schreibzugriffe sch¨ tzen. Die Bedingungsvariable re-
u
gelt den Zugriff auf die neu definierte RW-Sperrvariable.
Abbildung 4.2 gibt Funktionen zur Verwaltung von RW-
Sperrvariablen an. Die Funktion rw lock init() dient der
Initialisierung einer RW-Sperrvariable vom Typ rw lock t.
Die Funktion rw lock rlock() fordert einen Lesezugriff
auf die gemeinsame Datenstruktur an. Der Lesezugriff wird
nur dann gew¨hrt, wenn kein anderer Thread eine Schreib-
a
berechtigung erhalten hat. Hat ein anderer Thread ei-
ne Schreibberechtigung, wird der anfordernde Thread blo-
ckiert, bis die Schreibberechtigung wieder zur¨ ckgegeben
u
wird. Die Funktion rw lock wlock() dient der Anforde-
4.4 Erweiterter Sperrmechanismus 77
int rw lock init (rw lock t *rwl) {
rwl->num r = rwl->num w = 0;
pthread mutex init (&(rwl->mutex),NULL);
pthread cond init (&(rwl->cond),NULL);
return 0; }
int rw lock rlock (rw lock t *rwl) {
pthread mutex lock (&(rwl->mutex));
while (rwl->num w > 0)
pthread cond wait(&(rwl->cond),&(rwl->mutex));
rwl->num r ++;
pthread mutex unlock (&(rwl->mutex));
return 0; }
int rw lock wlock (rw lock t *rwl) {
pthread mutex lock (&(rwl->mutex));
while ((rwl->num w > 0) || (rwl->num r > 0))
pthread cond wait(&(rwl->cond),&(rwl->mutex));
rwl->num w ++;
pthread mutex unlock (&(rwl->mutex));
return 0; }
int rw lock runlock (rw lock t *rwl) {
pthread mutex lock (&(rwl->mutex));
rwl->num r --;
if (rwl->num r == 0)
pthread cond signal (&(rwl->cond));
pthread mutex unlock (&(rwl->mutex));
return 0; }
int rw lock wunlock (rw lock t *rwl) {
pthread mutex lock (&(rwl->mutex));
rwl->num w --;
pthread cond broadcast (&(rwl->cond));
pthread mutex unlock (&(rwl->mutex));
return 0; }
Abbildung 4.2. Funktionen zur Verwaltung von RW-
Sperrvariablen (read/write lock variables).
78 4 Programmierung mit Pthreads
rung einer Schreibberechtigung. Diese wird nur gew¨hrt,
a
wenn kein anderer Thread eine Lese- oder eine Schreibbe-
rechtigung erhalten hat.
Die Funktion rw lock runlock() dient der R¨ckgabe
u
einer Leseberechtigung. Sinkt durch die R¨ ckgabe einer Le-
u
seberechtigung die Anzahl der lesend zugreifenden Threads
auf Null, so wird ein auf eine Schreibberechtigung warten-
der Thread durch einen Aufruf von pthread cond signal()
aufgeweckt. Die Funktion rw lock wunlock() dient ent-
sprechend der R¨ ckgabe einer Schreibberechtigung. Da ma-
u
ximal ein schreibender Thread erlaubt ist, hat nach dieser
R¨ ckgabe kein Thread mehr eine Schreibberechtigung, und
u
alle auf einen Lesezugriff wartenden Threads k¨nnen durch
o
pthread cond broadcast() aufgeweckt werden.
Die skizzierte Implementierung von RW-Sperrvariablen
gibt Lesezugriffen Vorrang vor Schreibzugriffen: Wenn ein
Thread T1 eine Leseerlaubnis erhalten hat und Thread T2
auf eine Schreiberlaubnis wartet, erhalten andere Threads
auch dann eine Leseerlaubnis, wenn diese nach der Schrei-
berlaubnis von T2 beantragt wird. Thread T2 erh¨lt erst
a
dann eine Schreiberlaubnis, wenn kein anderer Thread mehr
eine Leseerlaubnis beantragt hat. Je nach Anwendung kann
es sinnvoll sein, den Schreibzugriffen Vorrang vor Lesezu-
griffen zu geben, damit die Datenstruktur immer auf dem
aktuellsten Stand ist. Wie dies erreicht werden kann, ist in
[11] skizziert.
4.5 Implementierung eines Taskpools
Eine naheliegende Gestaltung eines Thread-Programms be-
steht darin, f¨ r jede abzuarbeitende Aufgabe oder Funkti-
u
on (also allgemein Task) genau einen Thread zu erzeugen,
der diese Task abarbeitet und anschließend wieder zerst¨rt
o
4.5 Implementierung eines Taskpools 79
wird. Dies kann je nach Anwendung dazu f¨ hren, dass sehr
u
viele Threads erzeugt und wieder zerst¨rt werden, was einen
o
nicht unerheblichen Zeitaufwand verursachen kann, insbe-
sondere wenn jeweils pro Task nur wenige Berechnungen
auszuf¨ hren sind. Eine effizientere parallele Implementie-
u
rung kann mit Hilfe eines Taskpools erreicht werden, siehe
auch Kapitel 3. Die Idee eines Taskpools besteht darin, eine
Datenstruktur anzulegen, in der die noch abzuarbeitenden
Programmteile (Tasks) abgelegt sind. F¨ r die Abarbeitung
u
der Tasks wird eine feste Anzahl von Threads verwendet,
die zu Beginn des Programms vom Haupt-Thread erzeugt
werden und bis zum Ende des Programms existieren. F¨ r u
die Threads stellt der Taskpool eine gemeinsame Daten-
struktur dar, auf die sie zugreifen und die dort abgelegten
Tasks entnehmen und anschließend abarbeiten. W¨hrenda
der Abarbeitung einer Task kann ein Thread neue Tasks
erzeugen und diese in den Taskpool einf¨ gen. Die Abarbei-
u
tung des parallelen Programms ist beendet, wenn der Task-
pool leer ist und jeder Thread seine Tasks abgearbeitet hat.
Wir beschreiben im Folgenden eine einfache Implementie-
rung eines Taskpools, vgl. [49]. Weitere Implementierungen
sind zum Beispiel in [11, 35, 38, 30] beschrieben.
Abbildung 4.3 zeigt die Datenstruktur eines Taskpools
und die Funktion tpool init() zur Initialisierung des Task-
pools. Der Datentyp work t beschreibt eine einzelne Task
des Taskpools. Diese Beschreibung besteht aus je einem Zei-
ger auf die auszuf¨ hrende Funktion und auf das Argument
u
dieser Funktion. Die einzelnen Tasks sind durch Zeiger next
in Form einer einfach verketteten Liste miteinander ver-
bunden. Der Datentyp tpool t beschreibt die gesamte Da-
tenstruktur eines Taskpools. Dabei bezeichnet num thr die
Anzahl der f¨ r die Abarbeitung verwendeten Threads; das
u
Feld threads enth¨lt Zeiger auf die abarbeitenden Threads.
a
Die Eintr¨ge max size und current size geben die maxi-
a
80 4 Programmierung mit Pthreads
male bzw. aktuelle Anzahl von eingetragenen Tasks an. Die
Zeiger head und tail zeigen auf den Anfang bzw. das Ende
der Taskliste.
Die Mutexvariable lock wird verwendet, um den wech-
selseitigen Ausschluss beim Zugriff auf den Taskpool durch
die Threads sicherzustellen. Wenn ein Thread versucht, ei-
ne Task aus einem leeren Taskpool zu entnehmen, wird er
bzgl. der Bedingungsvariable not empty blockiert. F¨ gt ein
u
Thread einen Task in einen leeren Taskpool ein, wird ein
bzgl. der Bedingungsvariable not empty blockierter Thread
aufgeweckt. Wenn ein Thread versucht, eine Task in einen
vollen Taskpool einzuf¨ gen, wird er bzgl. der Bedingungsva-
u
riable not full blockiert. Entnimmt ein Thread eine Task
aus einem vollen Taskpool, wird ein evtl. bzgl. der Bedin-
gungsvariable not full blockierter Thread aufgeweckt.
Die Funktion tpool init() in Abbildung 4.3 initiali-
siert einen Taskpool, indem sie die Datenstruktur allokiert,
mit den als Argument mitgelieferten Werten initialisiert
und die zur Abarbeitung vorgesehene Anzahl von Threads
tpl->threads[i], i=0,...,num thr-1, erzeugt. Jeder die-
ser Threads erh¨lt eine Funktion tpool thread() als Star-
a
troutine, die einen Taskpool tpl als Argument hat.
Die in Abb. 4.4 angegebene Funktion tpool thread()
dient der Abarbeitung von im Taskpool abgelegten Tasks.
In jedem Durchlauf der Schleife von tpool thread() wird
versucht, eine Task vom Anfang der Taskliste des Task-
pools zu entnehmen. Wenn der Taskpool zur Zeit leer ist,
wird der ausf¨ hrende Thread bzgl. der Bedingungsvaria-
u
ble not empty blockiert. Sonst wird eine Task wl vom
Anfang der Taskschlange entnommen. War der Taskpool
vor der Entnahme voll, werden alle Threads, die blockiert
sind, weil sie eine Task abzulegen versuchen, mit einer
pthread cond broadcast()-Anweisung aufgeweckt. Die Zu-
griffe von tpool thread auf den Taskpool werden durch die
4.5 Implementierung eines Taskpools 81
typedef struct work {
void (*routine)();
void *arg;
struct work *next;
} work t;
typedef struct tpool {
int num thr, max size, current size;
pthread t *threads;
work t *head, *tail;
pthread mutex t lock;
pthread cond t not empty, not full;
} tpool t;
tpool t *tpool init (int num thr, int max size) {
int i;
tpool t *tpl;
tpl = (tpool t *) malloc (sizeof (tpool t));
tpl->num thr = num thr;
tpl->max size = max size;
tpl->current size = 0;
tpl->head = tpl->tail = NULL;
pthread mutex init (&(tpl->lock), NULL);
pthread cond init (&(tpl->not empty), NULL);
pthread cond init (&(tpl->not full), NULL);
tpl->threads = (pthread t *)
malloc(sizeof(pthread t)*num thr);
for (i=0; i<num thr; i++)
pthread create (&(tpl->threads[i]), NULL,
tpool thread, (void *) tpl);
return tpl;
}
Abbildung 4.3. Implementierung eines Taskpools: Datenstruk-
turen und Initialisierung.
82 4 Programmierung mit Pthreads
void *tpool thread (tpool t *tpl) {
work t *wl;
for( ; ; ) {
pthread mutex lock (&(tpl->lock));
while (tpl->current size == 0)
pthread cond wait (&(tpl->not empty),
&(tpl->lock));
wl = tpl->head;
tpl->current size --;
if (tpl->current size == 0)
tpl->head = tpl->tail = NULL;
else tpl->head = wl->next;
if (tpl->current size == tpl->max size - 1)
pthread cond broadcast (&(tpl->not full));
pthread mutex unlock (&(tpl->lock));
(*(wl->routine))(wl->arg);
free(wl);
}
}
Abbildung 4.4. Funktion tpool thread() zur Taskpoolimple-
mentierung.
Mutexvariable lock gesch¨ tzt. Die Abarbeitung der Funk-
u
tion routine einer entnommenen Task wl wird danach
ausgef¨ hrt. Diese Abarbeitung kann die Erzeugung neuer
u
Tasks beinhalten, die durch die Funktion tpool insert in
den Taskpool tpl eingetragen werden.
Die Funktion tpool insert() in Abbildung 4.5 f¨ gt ei-
u
ne Task in den Taskpool ein. Falls der Taskpool voll ist,
wird der ausf¨ hrende Thread bzgl. der Bedingungsvariable
u
not full blockiert. Ist der Taskpool nicht voll, so wird eine
Task mit den entsprechenden Daten belegt und an das En-
de der Taskschlange geh¨ngt. War diese vor dem Anh¨ngen
a a
4.5 Implementierung eines Taskpools 83
void tpool insert (tpool t *tpl,
void (*routine)(), void *arg) {
work t *wl;
pthread mutex lock (&(tpl->lock));
while (tpl->current size == tpl->max size)
pthread cond wait (&(tpl->not full),
&(tpl->lock));
wl = (work t *) malloc (sizeof (work t));
wl->routine = routine;
wl->arg = arg;
wl->next = NULL;
if (tpl->current size == 0) {
tpl->tail = tpl->head = wl;
pthread cond signal (&(tpl->not empty));
}
else {
tpl->tail->next = wl;
tpl->tail = wl;
}
tpl->current size ++;
pthread mutex unlock (&(tpl->lock));
}
Abbildung 4.5. Funktion tpool insert() zur Taskpoolimple-
mentierung.
leer, wird ein Thread, der bzgl. der Bedingungsvariable
not empty blockiert ist, aufgeweckt. Die Manipulationen
des Taskpools tpl werden wieder durch die Mutexvariable
gesch¨ tzt.
u
Die skizzierte Implementierung eines Taskpools ist ins-
besondere f¨ r ein Master-Slave-Modell geeignet, in dem ein
u
Master-Thread mit tpool init() die gew¨ nschte Anzahl
u
84 4 Programmierung mit Pthreads
von Slave-Threads erzeugt, von denen jeder die Funktion
tpool thread() abarbeitet. Die zu bearbeitenden Tasks
werden entsprechend der zu realisierenden Anwendung de-
finiert und k¨nnen vom Master-Thread durch Aufruf von
o
tpool insert() in den Taskpool eingetragen werden. Wer-
den bei der Bearbeitung einer Task neue Tasks erzeugt,
k¨nnen diese auch vom ausf¨ hrenden Slave-Thread einge-
o u
tragen werden. Die Beendigung der Slave-Threads nach
vollst¨ndiger Abarbeitung aller Tasks wird vom Master-
a
Thread ubernommen. Dazu werden alle bzgl. der beiden
¨
Bedingungsvariablen not empty und not full blockierten
Threads aufgeweckt und beendet. Sollte ein Thread gerade
eine Task bearbeiten, wird auf die Beendigung der Abar-
beitung gewartet bevor der Thread beendet wird.
5
Java-Threads
Die Entwicklung von aus mehreren Threads bestehenden
Programmen wird in der objektorientierten Programmier-
sprache Java auf Sprachebene unterst¨ tzt. Java stellt dazu
u
u.a. Sprachkonstrukte f¨ r die synchronisierte Ausf¨ hrung
u u
von Programmbereichen bereit und erlaubt die Erzeugung
und Verwaltung von Threads durch Verwendung vordefi-
nierter Klassen. Im Folgenden wird die Verwendung von
Java-Threads zur Entwicklung paralleler Programme f¨ r u
einen gemeinsamen Adressraum kurz vorgestellt, wobei nur
auf f¨ r Threads wesentliche Aspekte eingegangen wird. F¨ r
u u
eine ausf¨ hrliche Behandlung der Programmiersprache Ja-
u
va verweisen wir auf [22].
5.1 Erzeugung von Threads in Java
Jedes ausgef¨ hrte Java-Programm besteht aus mindestens
u
einem Thread, dem Haupt-Thread. Dieses ist der Thread,
der die main()-Methode der Klasse ausf¨ hrt, die als Star-
u
targument der Java Virtual Machine (JVM) angegeben
86 5 Java-Threads
wird. Weitere Benutzer-Threads werden von diesem Haupt-
Thread oder von bereits erzeugten Threads explizit er-
zeugt und gestartet. Dazu steht die vordefinierte Klasse
Thread aus dem Standardpaket java.lang zur Verf¨ gung,
u
die zur Repr¨sentation von Threads verwendet wird und
a
die Mechanismen und Methoden zur Erzeugung und Ver-
waltung von Threads bereitstellt. Das Interface Runnable
aus java.lang repr¨sentiert den von einem Thread aus-
a
zuf¨ hrenden Code, der in einer run()-Methode zur Verf¨ -
u u
gung gestellt wird. F¨ r die Definition einer run()-Methode,
u
die von einem Thread asynchron ausgef¨ hrt wird, gibt es
u
zwei M¨glichkeiten: das Erben von der Klasse Thread oder
o
die Implementierung des Interfaces Runnable.
Erben von der Klasse Thread
Bei diesem Vorgehen wird eine neue Klasse NewClass de-
finiert, die von der vordefinierten Klasse Thread erbt und
die enthaltene Methode run() mit den Anweisungen des
auszuf¨ hrenden Threads uberschreibt. Zus¨tzlich enth¨lt
u a a
¨
die Klasse Thread eine Methode start(), die einen neu-
en Thread erzeugt, der dann die Methode run() ausf¨hrt.u
Der neu erzeugte Thread wird asynchron zum aufrufenden
Thread ausgef¨hrt. Nach Ausf¨ hrung von start() wird die
u u
Kontrolle direkt an den aufrufenden Thread zur¨ ckgege-
u
ben. Dies erfolgt evtl. vor der Beendigung des neu erzeugten
Threads, so dass erzeugender und erzeugter Thread asyn-
chron zueinander arbeiten. Der neu erzeugte Thread termi-
niert, sobald seine run()-Methode vollst¨ndig abgearbeitet
a
ist.
Dieses Vorgehen ist in Abbildung 5.1 am Beispiel ei-
ner Klasse NewClass illustriert, deren main-Methode ein
Objekt der Klasse NewClass erzeugt und dessen run()-
Methode durch Aufruf von start aktiviert wird.
5.1 Erzeugung von Threads in Java 87
import java.lang.Thread;
public class NewClass extends Thread { // Vererbung
public void run() {
// ¨berschreiben von run() der Thread-Klasse
U
System.out.println(”hello from new thread”);
}
public static void main (String args[]) {
NewClass nc = new NewClass();
nc.start();
}
}
¨
Abbildung 5.1. Erzeugung eines Threads durch Uberschreiben
der run()-Methode der Klasse Thread.
Bei der gerade beschriebenen Methode zur Erzeugung
eines Threads muss die neue Klasse von der Klasse Thread
erben. Da Java keine Mehrfach-Vererbung zul¨sst, hat dies
a
den Nachteil, dass die neue Klasse von keiner weiteren
Klasse erben kann, was die Entwicklung von Anwendungs-
programmen einschr¨nkt. Dieser Nachteil der fehlenden
a
Mehrfach-Vererbung wird in Java durch die Bereitstel-
lung von Interfaces ausgeglichen, wof¨ r im Falle der Klasse
u
Thread das Interface Runnable genutzt wird.
Verwendung des Interface Runnable
Das Interface Runnable enth¨lt eine parameterlose run()-
a
Methode:
public interface Runnable {
public abstract void run();
}
88 5 Java-Threads
Die vordefinierte Klasse Thread implementiert das Inter-
face Runnable, d.h. jede von Thread abgeleitete Klasse im-
plementiert ebenfalls das Interface Runnable. Eine neu er-
zeugte Klasse NewClass kann daher auch direkt das In-
terface Runnable implementieren anstatt von der Klasse
Thread abgeleitet zu werden. Objekte einer solchen Klasse
NewClass sind aber keine Threadobjekte, so dass zur Er-
zeugung eines Threads immer noch ein Objekt der Klasse
Thread erzeugt werden muss, das allerdings als Parameter
ein Objekt der neuen Klasse NewClass hat. Dazu enth¨lt a
die Klasse Thread einen Konstruktor
public Thread (Runnable target).
Bei Verwendung dieses Konstruktors ruft die start()-
Methode von Thread die run()-Methode des Parameterob-
jektes vom Typ Runnable auf. Dies wird durch die run()-
Methode von Thread erreicht, die wie folgt definiert ist:
public void run() {
if (target != null) target.run();
}
Die run()-Methode wird in einem separaten, neu erzeugten
Thread asynchron zum aufrufenden Thread ausgef¨hrt. Die
u
Erzeugung eines neuen Threads kann somit in drei Schritten
erfolgen:
(1) Definition einer neuen Klasse NewClass, die Runnable
implementiert und f¨ r die eine run()-Methode definiert
u
wird, die die von dem neu zu erzeugenden Thread aus-
zuf¨ hrende Anweisungsfolge enth¨lt;
u a
(2) Erzeugung eines Objektes der Klasse Thread mit Hilfe
des Konstruktors Thread(Runnable target) und ei-
¨
nes Objektes der Klasse NewClass sowie Ubergabe die-
ses Objektes an den Thread-Konstruktor;
(3) Aufruf der start()-Methode des Thread-Objektes.
5.1 Erzeugung von Threads in Java 89
Dieses Vorgehen ist in Abbildung 5.2 am Beispiel einer
Klasse NewClass illustriert. Ein Objekt dieser Klasse wird
dem Konstruktor von Thread als Parameter ubergeben.
¨
import java.lang.Thread;
public class NewClass implements Runnable {
public void run() {
System.out.println(”hello from new thread”);
}
public static void main (String args[]) {
NewClass nc = new NewClass();
Thread th = new Thread(nc);
th.start(); // start() ruft nc.run() auf
}
}
Abbildung 5.2. Erzeugung eines Threads mit Hilfe des Interface
Runnable und Verwendung einer neuen Klasse NewClass.
Weitere Methoden der Klasse Thread
Ein Java-Thread kann auf die Beendigung eines anderen
Java-Threads t warten, indem er t.join() aufruft. Durch
diesen Aufruf blockiert der aufrufende Thread so lange, bis
der Thread t beendet ist. Die join()-Methode wird in drei
Varianten zur Verf¨ gung gestellt:
u
• void join(): der aufrufende Thread wird blockiert, bis
der angegebene Thread beendet ist;
• void join(long timeout): der aufrufende Thread wird
blockiert; die Blockierung wird aufgehoben, sobald der
90 5 Java-Threads
angegebene Thread beendet ist oder wenn die angegebe-
ne Zeit timeout abgelaufen ist (Angabe in Millisekun-
den);
• void join(long timeout, int nanos): das Verhalten
entspricht dem von void join(long timeout); der zu-
s¨tzliche Parameter erm¨glicht eine genauere Angabe
a o
des Zeitintervalls durch die zus¨tzliche Angabe von Na-
a
nosekunden.
Wurde der angegebene Thread noch nicht gestartet, findet
bei keiner der join()-Varianten eine Blockierung statt.
Die Methode
boolean isAlive()
der Klasse Thread erm¨glicht die Abfrage des Ausf¨ hrungs-
o u
status eines Threads: die Methode liefert true zur¨ ck, falls
u
der angegebene Thread gestartet wurde, aber noch nicht
beendet ist. Weder die isAlive()-Methode noch die ver-
schiedenen Varianten der join-Methode haben einen Ein-
fluss auf den Thread, der Ziel des Aufrufes ist. Nur der
ausf¨ hrende Thread ist betroffen.
u
Die Thread-Klasse definiert einige statische Methoden,
die den aktuell ausgef¨ hrten Thread betreffen oder Infor-
u
mationen uber das Gesamtprogramm liefern.
¨
Da diese Methoden statisch sind, k¨nnen sie aufgerufen wer-
o
den, auch wenn kein Objekt der Klasse Thread verwendet
wird. Der Aufruf der Methode
static Thread currentThread();
liefert eine Referenz auf das Thread-Objekt des aufrufenden
Threads. Diese Referenz kann z.B. dazu verwendet werden,
nicht-statische Methoden dieses Thread-Objektes aufzuru-
fen. Die Methode
static void sleep (long milliseconds);
5.2 Synchronisation von Java-Threads 91
blockiert den ausf¨ hrenden Thread vor¨ bergehend f¨ r die
u u u
angegebene Anzahl von Millisekunden, d.h. der Prozessor
kann einem anderen Thread zugeteilt werden. Nach Ablauf
des Zeitintervalls wird der Thread wieder ausf¨ hrungsbereit
u
und kann wieder einem Prozessor zur weiteren Ausf¨ hrung
u
zugeteilt werden. Die Methode
static void yield();
ist ein Hinweis an die Java Virtual Machine (JVM), dass ein
anderer ausf¨ hrungsbereiter Thread gleicher Priorit¨t dem
u a
Prozessor zugeteilt werden soll. Wenn ein solcher Thread
existiert, kann der Scheduler der JVM diesen zur Ausf¨ h-
u
rung bringen. Die Anwendung von yield() ist sinnvoll f¨ ru
JVM-Implementierungen ohne Scheduling mit Zeitschei-
benverfahren, falls Threads langlaufende Berechnungen oh-
ne Blockierungsm¨glichkeit ausf¨ hren. Die Methode
o u
static int enumerate (Thread[] th_array);
liefert eine Liste aller Thread-Objekte des Programms.
Der R¨ ckgabewert gibt die Anzahl der im Parameterfeld
u
th array abgelegten Thread-Objekte an. Mit der Methode
static int activeCount();
kann die Anzahl der Thread-Objekte des Programms be-
stimmt werden. Die Methode kann z.B. verwendet werden,
um vor Aufruf von enumerate() die erforderliche Gr¨ße des
o
Parameterfeldes zu ermitteln.
5.2 Synchronisation von Java-Threads
Die Threads eines Java-Programms arbeiten auf einem ge-
meinsamen Adressraum. Wenn auf Variablen durch mehre-
re Threads zugegriffen werden kann, m¨ ssen also zur Ver-
u
92 5 Java-Threads
meidung zeitkritischer Abl¨ufe geeignete Synchronisations-
a
mechanismen angewendet werden. Zur Sicherstellung des
wechselseitigen Ausschlusses von Threads beim Zugriff auf
gemeinsame Daten stellt Java synchronized-Bl¨cke und
o
-Methoden zur Verf¨ gung. Wird ein Block oder eine Me-
u
thode als synchronized deklariert, ist sichergestellt, dass
keine gleichzeitige Ausf¨ hrung durch zwei Threads erfol-
u
gen kann. Eine Datenstruktur kann also dadurch vor kon-
kurrierenden Zugriffen mehrerer Threads gesch¨ tzt werden,
u
dass alle Zugriffe auf die Datenstruktur in synchronized
Methoden oder Bl¨cken erfolgen. Die synchronisierte Inkre-
o
mentierung eines Z¨hlers kann beispielsweise durch folgende
a
Methode incr() realisiert werden:
public class Counter {
private int value = 0;
public synchronized int incr() {
value = value + 1;
return value;
}
}
In der JVM wird die Synchronisation dadurch realisiert,
dass jedem Java-Objekt implizit eine Mutexvariable zu-
geordnet wird. Jedes Objekt der allgemeinen Klasse Object
besitzt eine solche implizite Mutexvariable. Da jede Klasse
direkt oder indirekt von der Klasse Object abgeleitet ist,
besitzt somit jedes Objekt eine Mutexvariable. Der Aufruf
einer synchronized-Methode bez¨ glich eines Objektes Ob
u
hat den folgenden Effekt:
• Beim Start der synchronized-Methode durch einen
Thread t wird die Mutexvariable von Ob implizit be-
legt. Wenn die Mutexvariable bereits von einem anderen
Thread belegt ist, wird der ausf¨ hrende Thread t blo-
u
ckiert. Der blockierte Thread wird wieder ausf¨ hrungs-
u
5.2 Synchronisation von Java-Threads 93
bereit, wenn die Mutexvariable freigegeben wird. Die
aufgerufene synchronized-Methode wird nur bei erfolg-
reicher Sperrung der Mutexvariablen von Ob ausgef¨hrt.
u
• Beim Verlassen der Methode wird die Mutexvariable
von Ob implizit wieder freigegeben und kann damit von
einem anderen Thread gesperrt werden.
Damit kann ein synchronisierter Zugriff auf ein Objekt da-
durch realisiert werden, dass alle Methoden, die konkur-
rierend durch mehrere Threads aufgerufen werden k¨nnen,
o
als synchronized deklariert werden. Zur Sicherstellung
des wechselseitigen Ausschlusses ist es wichtig, dass nur
uber diese Methoden auf das zu sch¨ tzende Objekt zuge-
u
¨
griffen wird. Neben synchronized-Methoden k¨nnen auch
o
synchronized-Bl¨cke verwendet werden. Dies ist dann sinn-
o
voll, wenn nur ein Teil einer Methode auf kritische Daten
zugreift, eine synchronisierte Ausf¨ hrung der gesamten Me-
u
thode aber nicht notwendig erscheint. Bei synchronized-
Bl¨cken erfolgt die Synchronisation meist bez¨ glich des Ob-
o u
jektes, in dessen Methode der synchronized-Block steht.
Die obige Methode zur Inkrementierung eines Z¨hlers kann
a
mit Hilfe eines synchronized-Blocks folgendermaßen for-
muliert werden:
public int incr() {
synchronized (this) {
value = value + 1; return value;
}
}
Der Synchronisationsmechanismus von Java kann zur Rea-
lisierung voll-synchronisierter Objekte, auch atomare
Objekte genannt, verwendet werden, die von einer beliebi-
gen Anzahl von Threads ohne Synchronisation zugegriffen
werden k¨nnen. Damit dabei keine zeitkritischen Abl¨ufe
o a
entstehen, muss die Synchronisation in der definierenden
94 5 Java-Threads
Klasse enthalten sein. Diese muss folgende Bedingungen
erf¨ llen:
u
• alle Methoden m¨ ssen synchronized sein,
u
• es d¨ rfen keine public-Felder enthalten sein, die ohne
u
Aufruf einer Methode zugegriffen werden k¨nnen,
o
• alle Felder werden in Konstruktoren der Klasse konsis-
tent initialisiert,
• der Zustand der Objekte bleibt auch beim Auftreten
von Ausnahmen in einem konsistenten Zustand.
Abbildung 5.3 zeigt das Konzept voll-synchronisierter
Objekte am Beispiel einer Klasse ExpandableArray, die ei-
ne vereinfachte Version der vordefinierten synchronisierten
Klasse java.util.Vector ist, vgl. auch [40]. Die Klasse
realisiert ein adaptierbares Feld mit beliebigen Objekten,
dessen Gr¨ße entsprechend der Anzahl abgelegter Objek-
o
te wachsen oder schrumpfen kann. Dies ist in der Methode
add() realisiert: wird beim Hinzuf¨ gen eines neuen Ele-
u
mentes festgestellt, dass das Feld data voll belegt ist, wird
dieses entsprechend vergr¨ßert. Dazu wird ein gr¨ßeres Feld
o o
neu angelegt und das bisherige Feld wird mit Hilfe der Me-
thode arraycopy() aus der System-Klasse umkopiert. Oh-
ne die Synchronisationsoperationen k¨nnte die Klasse nicht
o
sicher von mehreren Threads gleichzeitig genutzt werden.
Ein Konflikt k¨nnte z.B. auftreten, wenn zwei Threads zum
o
gleichen Zeitpunkt versuchen, eine add-Operation durch-
zuf¨ hren.
u
Auftreten von Deadlocks
Die Verwendung voll synchronisierter Klassen vermeidet
zwar das Auftreten zeitkritischer Abl¨ufe, es k¨nnen aber
a o
Deadlocks auftreten, wenn Threads bzgl. mehrerer Objekte
5.2 Synchronisation von Java-Threads 95
import java.lang.*;
import java.util.*;
public class ExpandableArray {
private Object[] data;
private int size = 0;
public ExpandableArray(int cap) {
data = new Object[cap];
}
public synchronized int size() {
return size;
}
public synchronized Object get(int i)
throws NoSuchElementException {
if (i < 0 || i >= size)
throw new NoSuchElementException();
return data[i];
}
public synchronized void add(Object x) {
if (size == data.length) { // Feld zu klein
Object[] od = data;
data = new Object[3 * (size + 1) / 2];
System.arraycopy(od, 0, data, 0, od.length);
}
data[size++] = x;
}
public synchronized void removeLast()
throws NoSuchElementException {
if (size == 0)
throw new NoSuchElementException();
data[--size] = null;
}
}
Abbildung 5.3. Beispiel f¨r eine voll synchronisierte Klasse.
u
96 5 Java-Threads
pulic class Account {
private long balance;
synchronized long getBalance() {return balance;}
synchronized void setBalance(long v) {
balance = v;
}
synchronized void swapBalance(Account other) {
long t = getBalance();
long v = other.getBalance();
setBalance(v);
other.setBalance(t);
}
}
Abbildung 5.4. Beispiel f¨r das Auftreten eines Deadlocks.
u
synchronisiert werden. Dies ist in Abb. 5.4 am Beispiel ei-
nes Kontos (Klasse Account) veranschaulicht, bei dem die
Methode swapBalance() die Kontost¨nde austauscht, vgl.
a
auch [40]. Bei der Bearbeitung von swapBalance() ist beim
Einsatz von zwei Threads T1 und T2 das Auftreten eines
Deadlocks m¨glich, wenn ein Thread a.swapBalance(b),
o
der andere Thread b.swapBalance(a) aufruft und die bei-
den Threads auf unterschiedlichen Prozessorkernen eines
Prozessors ablaufen. Der Deadlock tritt bei folgender Ab-
arbeitungsreihenfolge auf:
(A) Zeitpunkt 1: Thread T1 ruft a.swapBalance(b) auf
und erh¨lt die Mutexvariable von Objekt a;
a
(B) Zeitpunkt 2: Thread T1 ruft getBalance() f¨ r Objekt
u
a auf und f¨ hrt die Funktion aus;
u
(C) Zeitpunkt 2: Thread T2 ruft b.swapBalance(a) auf
und erh¨lt die Mutexvariable von Objekt b;
a
5.2 Synchronisation von Java-Threads 97
(D) Zeitpunkt 3: Thread T1 ruft b.getBalance() auf und
blockiert bzgl. der Mutexvariable von Objekt b;
(E) Zeitpunkt 3: Thread T2 ruft getBalance() F¨ r Ob-
u
jekt b auf auf f¨ hrt die Funktion aus;
u
(F) Zeitpunkt 4: Thread T2 ruft a.getBalance() auf und
blockiert bzgl. der Mutexvariable von Objekt a.
Der Ablauf ist in Abb. 5.5 veranschaulicht. Zum Zeitpunkt
4 sind beide Thread blockiert: Thread T1 ist bzgl. der Mu-
texvariable von b blockiert. Diese ist von Thread T2 belegt
und kann nur von Thread T2 freigegeben werden. Thread
T2 ist bzgl. der Mutexvariablen von a blockiert, die nur von
Thread T1 freigegeben werden kann. Somit warten die bei-
den Threads gegenseitig aufeinander und es ist ein Deadlock
eingetreten.
Thread T1 Thread T2
Zeit-
punkt
1 a.swapBalance(b)
2 t = getBalance() b.swapBalance(a)
3 Blockierung bzgl. b t = getBalance()
4 Blockierung bzgl. a
Abbildung 5.5. Deadlockablauf zu Abb. 5.4.
Deadlocks treten typischerweise dann auf, wenn unter-
schiedliche Threads die Mutexvariablen derselben Objek-
te in unterschiedlicher Reihenfolge zu sperren versuchen.
Im Beispiel von Abb. 5.5 versucht Thread T1 zuerst a und
dann b zu sperren, Thread T2 versucht das Sperren in um-
gekehrter Reihenfolge. In dieser Situation kann das Auf-
treten eines Deadlocks dadurch vermieden werden, dass
die beteiligten Threads die Objekte immer in der glei-
chen Reihenfolge zu sperren versuchen. In Java kann diese
98 5 Java-Threads
dadurch realisiert werden, dass die zu sperrenden Objek-
te beim Sperren eindeutig angeordnet werden; dazu kann
z.B. die Methode System.identityHashCode() verwendet
werden, die sich immer auf die Default-Implementierung
Object.hashCode() bezieht [40]; diese liefert eine eindeu-
tige Indentifizierung des Objektes. Es kann aber auch ei-
ne beliebige andere eindeutige Anordnung der Objekte ver-
wendet werden.
Damit kann eine alternative Formulierung von swap-
Balance() angegeben werden, bei der keine Deadlocks auf-
treten k¨nnen, vgl. Abb. 5.6. Die neue Formulierung enth¨lt
o a
¨
auch eine Alias-Uberpr¨ fung, so dass die Operation nur
u
ausgef¨ hrt wird, wenn unterschiedliche Objekte beteiligt
u
sind. Die Methode swapBalance() ist jetzt nicht mehr als
synchronized deklariert.
public void swapBalance(Account other) {
if (other == this) return;
else if (System.identityHashCode(this) <
System.identityHashCode(other))
this.doSwap(other);
else other.doSwap(this);
}
protected synchronized void doSwap(Account other) {
long t = getBalance();
long v = other.getBalance();
setBalance(v);
other.setBalance(t);
}
Abbildung 5.6. Deadlockfreie Realisierung von swapBalance()
aus Abb. 5.4.
5.2 Synchronisation von Java-Threads 99
Bei der Synchronisation von Java-Methoden sollten ein
paar Hinweise beachtet werden, die die resultierenden Pro-
gramme effizienter und sicherer machen:
• Synchronisation ist teuer. Synchronisierte Methoden soll-
ten daher nur dann verwendet werden, wenn die Metho-
den von mehreren Threads aufgerufen werden kann und
wenn innerhalb der Methoden gemeinsame Objektdaten
ver¨ndert werden k¨nnen. Wenn f¨ r die Anwendung si-
a o u
chergestellt ist, dass eine Methode jeweils nur von einem
Thread zugegriffen wird, kann eine Synchronisation zur
Erh¨hung der Effizienz vermieden werden.
o
• Die Synchronisation sollte auf die kritischen Bereiche
beschr¨nkt werden, um so die Zeit der Sperrung von
a
Objekten zu reduzieren. Anstelle von synchronized-
Methoden sollten bevorzugt synchronized-Bl¨cke ver-
o
wendet werden.
• Die Mutexvariable eines Objektes sollte nicht zur Synch-
ronisation nicht zusammenh¨ngender kritischer Berei-
a
che verwendet werden, da dies zu unn¨tigen Sequentia-
o
lisierungen f¨ hren kann.
u
• Einige Java-Klassen sind bereits intern synchronisiert;
Beispiele sind Hashtable, Vector und StringBuffer.
Zus¨tzliche Synchronisation ist f¨ r Objekte dieser Klas-
a u
sen also uber߬ ssig.
u
¨
• Ist f¨ r ein Objekt Synchronisation erforderlich, sollten
u
die Daten in private oder protected Feldern abge-
legt werden, damit kein unsynchronisierter Zugriff von
außen m¨glich ist; alle zugreifenden Methoden m¨ ssen
o u
synchronized sein.
• Greifen Threads eines Programms in unterschiedlicher
Reihenfolge auf Objekte zu, k¨nnen Deadlocks durch
o
Verwendung der gleichen Sperr-Reihenfolge verhindert
werden.
100 5 Java-Threads
Die Realisierung von synchronized-Bl¨cken mit Hilfe
o
der impliziten Mutexvariablen, die jedem Objekt zugeord-
net sind, funktioniert f¨ r alle Methoden, die bzgl. eines Ob-
u
jektes aktiviert werden. Statische Methoden einer Klasse
werden jedoch nicht bzgl. eines speziellen Objektes akti-
viert und eine implizite Objekt-Mutexvariable existiert da-
her nicht. Nichtsdestotrotz k¨nnen auch statische Metho-
o
den als synchronized deklariert werden. Die Synchroni-
sation erfolgt dann uber die Mutexvariable des zugeh¨ri- o
¨
gen Klassenobjektes der Klasse java.lang.Class, das f¨ r u
die Klasse, in der die statische Methode deklariert wird,
automatisch erzeugt wird. Statische und nicht-statische
synchronized Methoden einer Klasse verwenden also un-
terschiedliche Mutexvariablen f¨ r die Synchronisation.
u
Eine statische synchronized-Methode kann sowohl die
Mutexvariable der Klasse als auch die Mutexvariable eines
Objektes der Klasse sperren, indem sie eine nicht-statische
Methode bzgl. eines Objektes der Klasse aufruft oder ein
Objekt der Klasse zur Synchronisation nutzt. Dies wird in
Abb. 5.7 anhand der Klasse MyStatic illustriert.
Eine nicht-statische synchronized Methode kann durch
den Aufruf einer statischen synchronized Methode eben-
falls neben der Objekt-Mutexvariablen auch die Klassen-
Mutexvariable sperren. F¨ r eine Klasse Cl kann die Synch-
u
ronisation bzgl. der Klassen-Mutexvariablen auch direkt
durch
synchronized (Cl.class) { /* Rumpf*/ }
erfolgen.
5.3 Signalmechanismus in Java 101
public class MyStatic {
public static synchronized
void staticMethod(MyStatic obj) {
// hier wird die Klassen-Sperre verwendet
obj.nonStaticMethod();
synchronized(obj) {
// zus¨tzliche Verwendung der Objekt-Sperre
a
}
}
public synchronized void nonStaticMethod() {
// Verwendung der Objekt-Sperre
}
}
Abbildung 5.7. Synchronisation von statischen Methoden.
5.3 Signalmechanismus in Java
In manchen Situationen ist es sinnvoll, dass ein Thread
auf das Eintreten einer anwendungsspezifischen Bedingung
wartet. Sobald die Bedingung erf¨ llt ist, f¨ hrt der Thread
u u
eine festgelegte Aktion aus. So lange die Bedingung noch
nicht erf¨ llt ist, wartet der Thread darauf, dass ein anderer
u
Thread durch entsprechende Berechnungen das Eintreten
der Bedingung herbeif¨ hrt. In Pthreads konnten f¨ r solche
u u
Situationen Bedingungsvariablen eingesetzt werden. Java
stellt uber die Methoden wait() und notify(), die in der
¨
vordefinierten Klasse Object deklariert sind, einen ¨hnli-a
chen Mechanismus zur Verf¨ gung. Diese Methoden stehen
u
somit f¨ r jedes Objekt zur Verf¨ gung, da jedes Objekt di-
u u
rekt oder indirekt von der Klasse Object abgeleitet ist.
Beide Methoden d¨ rfen nur innerhalb eines synchronized-
u
Blocks oder einer synchronized-Methode aufgerufen wer-
den. Das typische Verwendungsmuster f¨ r wait() ist:
u
102 5 Java-Threads
synchronized (lockObject) {
while (!Bedingung) { lockObject.wait(); }
Aktion;
}
Der Aufruf von wait() blockiert den aufrufenden Thread
so lange, bis er von einem anderen Thread per notify()
aufgeweckt wird. Die Blockierung bewirkt auch die Frei-
gabe der impliziten Mutexvariable des Objektes, bzgl. der
der Thread synchronisiert. Damit kann diese Mutexvaria-
ble von einem anderen Thread gesperrt werden. Ein Auf-
ruf von notify() weckt einen bez¨ glich des zugeh¨rigen
u o
Objektes blockierten Thread auf. Der aufgeweckte Thread
wird ausf¨ hrungsbereit und versucht, die Kontrolle uber die
u ¨
implizite Mutexvariable des Objektes wieder zu erhalten.
Erst wenn ihm dies gelingt, f¨ hrt er die nach Eintreten der
u
Bedingung durchzuf¨ hrende Aktion aus. Wenn dies nicht
u
gelingt, blockiert der Thread bzgl. der Mutexvariablen, bis
diese von dem Thread, der sie gesperrt hat, wieder freige-
geben wird.
Die Arbeitsweise von wait() und notify() ¨hnelt der
a
Arbeitsweise von Pthread-Bedingungsvariablen und den
Operationen pthread cond wait() und pthread cond -
signal(), vgl. Seite 70. Die Implementierung von wait()
und notify() erfolgt mit Hilfe einer impliziten Warteliste,
in der f¨ r jedes Objekt eine Menge von wartenden Thre-
u
ads gehalten wird. Die Warteliste enth¨lt jeweils die Thre-
a
ads, die zum aktuellen Zeitpunkt durch Aufruf von wait()
bez¨ glich dieses Objektes blockiert wurden. Nicht in der
u
Warteliste enthalten sind die Threads, die blockiert wurden,
weil sie auf Zuteilung der impliziten Mutexvariable des Ob-
jektes warten. Welcher der Threads in der impliziten Warte-
liste beim Aufruf von notify() aufgeweckt wird, ist von der
Java-Sprachspezifikation nicht festgelegt. Mit Hilfe der Me-
5.3 Signalmechanismus in Java 103
thode notifyAll() werden alle in der Warteliste abgeleg-
ten Threads aufgeweckt und ausf¨ hrungsbereit; die analoge
u
Pthreads-Funktion ist pthread cond broadcast(). Eben-
so wie notify() muss notifyAll() in einem synchronized-
Block oder einer synchronized-Methode aufgerufen wer-
den.
Produzenten-Konsumenten-Muster
Der Java-Signalmechanismus kann etwa zur Realisierung ei-
nes Produzenten-Konsumenten-Musters mit Ablage- bzw.
Entnahmepuffer fester Gr¨ße verwendet werden, in den
o
Produzenten-Threads Datenobjekte ablegen und aus dem
Konsumenten-Threads Daten zur Weiterverarbeitung ent-
nehmen k¨nnen.
o
Abbildung 5.8 zeigt eine threadsichere Implementierung
eines Puffermechanismus mit Hilfe des Java-Signalmecha-
nismus, vgl. auch [40]. Beim Erzeugen eines Objektes vom
Typ BoundedBufferSignal wird ein Feld array vorgegebe-
ner Gr¨ße capacity erzeugt, das als Puffer dient. Zentrale
o
Methoden der Klasse sind put() zur Ablage eines Daten-
objektes im Puffer und take() zur Entnahme eines Date-
nobjektes aus dem Puffer. Ein Pufferobjekt kann in einem
der drei Zust¨nde voll, teilweise voll und leer sein, siehe
a
¨
5.9 f¨ r eine Veranschaulichung der Uberg¨nge zwischen den
u a
Zust¨nden. Die Zust¨nde sind durch folgende Bedingungen
a a
charakterisiert:
Zustand Bedingung put take
m¨glich m¨glich
o o
voll nein ja
size == capacity
teilweise voll 0 < size < capacity ja ja
leer ja nein
size == 0
104 5 Java-Threads
public class BoundedBufferSignal {
private final Object[] array;
private int putptr = 0;
private int takeptr = 0;
private int numel = 0;
public BoundedBufferSignal (int capacity)
throws IllegalArgumentException {
if (capacity <= 0)
throw new IllegalArgumentException();
array = new Object[capacity];
}
public synchronized int size() {return numel; }
public int capacity() {return array.length;}
public synchronized void put(Object obj)
throws InterruptedException {
while (numel == array.length)
wait(); // Puffer voll
array [putptr] = obj;
putptr = (putptr +1) % array.length;
if (numel++ == 0)
notifyAll(); // alle Threads aufwecken
}
public synchronized Object take()
throws InterruptedException {
while (numel == 0)
wait(); // Puffer leer
Object x = array [takeptr];
takeptr = (takeptr +1) % array.length;
if (numel-- == array.length)
notifyAll(); // alle Threads aufwecken
return x;
}
}
Abbildung 5.8. Realisierung eines threadsicheren Puffers mit
dem Java-Signalmechanismus.
5.3 Signalmechanismus in Java 105
take take
teilweise
voll leer
voll
put put
Abbildung 5.9. Veranschaulichung der Zust¨nde eines threadsi-
a
cheren Puffermechanismus.
Bei der Ausf¨ hrung einer put()-Operation durch einen
u
Produzenten-Thread wird dieser mittels wait() blockiert,
wenn der Puffer voll ist. Wird eine put()-Operation auf
einem vorher leeren Puffer ausgef¨ hrt, werden nach Ablage
u
des Datenobjektes alle wartenden (Konsumenten)-Threads
mit notifyAll() aufgeweckt.
Bei der Ausf¨ hrung einer take()-Operation durch einen
u
Konsumenten-Thread wird dieser mit wait() blockiert,
wenn der Puffer leer ist. Wird eine take()-Operation auf
einem vorher vollen Puffer ausgef¨ hrt, werden nach Ent-
u
nahme des Datenobjektes alle wartenden (Produzenten)-
Threads mit notifyAll() aufgeweckt. Die Implementie-
rung von put() und take() stellt sicher, dass ein Objekt
der Klasse BoundedPufferSignal von einer beliebigen An-
zahl von Threads zugegriffen werden kann, ohne dass zeit-
kritische Abl¨ufe entstehen.
a
Weitere Methoden
Die Klasse Object stellt zwei Varianten von wait() zur
Verf¨ gung, die die Angabe einer maximalen Wartezeit in
u
Millisekunden bzw. zus¨tzlichen Nanosekunden erlauben:
a
void wait (long msecs)
void wait (long msecs, int nanos)
Beide Varianten haben den gleichen Effekt wie wait() ohne
Parameter mit dem Unterschied, dass die Blockierung des
106 5 Java-Threads
Threads automatisch aufgehoben wird, sobald das als Pa-
rameter angegebene Zeitintervall msecs abgelaufen ist. Da
diese beiden Varianten ebenfalls in einem synchronized-
Block oder einer synchronized-Methode stehen m¨ ssen,u
versucht ein wegen des Ablaufs des Zeitintervalls aufge-
weckter Thread nach dem Aufwecken zuerst, die Kontrol-
le uber die implizite Mutexvariable des Objektes zu er-
¨
halten. Wenn dies nicht gelingt, wird er bzgl. dieser Mu-
texvariable blockiert. Durch die daraus evtl. resultierende
Wartezeit besteht keine Garantie daf¨ r, dass der vorher
u
blockierte Thread nach Ablauf des angegebenen Zeitinter-
valls tats¨chlich wieder zur Ausf¨ hrung kommt. Es kann
a u
auch keine Obergrenze f¨ r die zus¨tzliche Wartezeit ange-
u a
geben werden. Es gibt f¨ r den aufgeweckten Thread auch
u
keine M¨glichkeit festzustellen, ob er durch Ablauf des an-
o
gegebenen Zeitintervalls oder durch Aufruf von notify()
durch einen anderen Thread aufgeweckt wurde. Die Auf-
rufe wait(0) bzw. wait(0,0) sind ¨quivalent zum Aufruf
a
wait() ohne Parameter.
Ein durch einen Aufruf von wait(), sleep() oder join()
blockierter Thread kann auch dadurch wieder aufgeweckt
werden, dass er von einem anderen Thread unterbrochen
wird. Dazu steht die Methode
void interrupt()
der Klasse Thread zur Verf¨ gung. Durch Aufruf dieser
u
Methode wird der blockierte Thread mit der Ausnahme
InterruptedException aufgeweckt, die gem¨ß der ubli-
a ¨
chen Regeln f¨ r die Ausnahmebehandlung verarbeitet wer-
u
den kann. Dies wird von den Methoden put() und take()
in Abb. 5.8 ber¨ cksichtigt. Auf einen nicht blockierten
u
Thread t hat der Aufruf von t.interrupt() den Effekt,
dass das Interrupt-Flag des Threads t auf true gesetzt
5.3 Signalmechanismus in Java 107
wird. Ist das Interrupt-Flag eines Threads t auf true ge-
setzt, wird bei einem Aufruf von wait(), join() oder
sleep() durch diesen Thread direkt die Ausnahme Inter-
ruptedException ausgel¨st. Ein Thread kann seinen eige-
o
nen Interrupt-Status durch Aufruf der statischen Methode
static boolean interrupted()
der Klasse Thread uberpr¨ fen. Der Interrupt-Status eines
u
¨
beliebigen Threads kann durch Aufruf der nicht-statischen
Methode
boolean isInterrupted()
f¨ r das entsprechende Objekt der Klasse Thread abgefragt
u
werden. Es ist zu beachten, dass das Unterbrechen eines
Threads mit interrupt() nicht unbedingt seine Terminie-
rung nach sich zieht, obwohl dies f¨ r die meisten Anwendun-
u
gen der Normalfall ist. Ein vorher nicht blockierter Thread
kann aber trotz gesetztem Interrupt-Flag weiterarbeiten,
um dadurch z.B. vor seiner Terminierung einen konsisten-
ten Zustand zu hinterlassen. Die Methoden
static void sleep (long msecs)
static void sleep (long msecs, int nanos)
der Klasse Thread suspendieren den ausf¨ hrenden Thread
u
f¨ r das angegebene Zeitintervall. Im Unterschied zu wait()
u
muss sleep() aber nicht in einem synchronized-Block
stehen. Ein Aufruf von sleep() hat auch keinen Einfluss
auf eine entl. vom ausf¨ hrenden Thread gesperrte implizi-
u
te Mutexvariable eines Objektes. Wenn sleep() in einem
synchronized Block steht, f¨ hrt der Aufruf von sleep()
u
also nicht zur impliziten Freigabe der Mutexvariable des
zugeh¨rigen Objektes, und die Mutexvariable bleibt in die-
o
sem Fall w¨hrend der Wartezeit des Threads gesperrt. Nach
a
Ablauf des Zeitintervalls muss der ausf¨ hrende Thread, im
u
108 5 Java-Threads
Unterschied zu wait(), also auch nicht versuchen, die Kon-
trolle uber die Mutexvariable des Objektes zu erhalten, son-
¨
dern wird direkt ausf¨ hrungsbereit.
u
Die Methoden wait() und notify() sind nicht-statische
Methoden der Klasse Object und k¨nnen daher durch
o
statische nicht direkt aufgerufen werden, da es f¨ r stati-
u
sche Methoden keine zugeh¨rige Objektreferenz gibt. Um
o
wait() bzw. notify() in statischen Methoden verwenden
zu k¨nnen, muss ein zus¨tzliches Objekt erzeugt werden,
o a
bez¨ glich dem die Synchronisation in Form von wait()
u
und notify() durchgef¨ hrt werden kann. Dies kann ein
u
beliebiges Objekt der Klasse Object sein, aber auch das
Class-Objekt der Klasse, in der die zu synchronisierenden
statischen Methoden enthalten sind. Dies ist in Abbildung
5.10 am Beispiel einer Klasse mit zwei statischen Methoden
illustriert.
public class MyStaticClass {
public static void staticWait()
throws InterruptedException {
synchronized(MyStaticClass.class) {
MyStaticClass.class.wait();
}
}
public static void staticNotify() {
synchronized(MyStaticClass.class) {
MyStaticClass.class.notify();
}
}
}
Abbildung 5.10. Beispiel zur Synchronisation statischer Metho-
den mit wait() und notify().
5.4 Erweiterte Synchronisationsmuster 109
5.4 Erweiterte Synchronisationsmuster
Die vorgestellten Synchronisationsmechanismen f¨ r Java-
u
Threads k¨nnen dazu verwendet werden, komplexere Syn-
o
chronisationsmuster zu realisieren, die h¨ufig in parallelen
a
Anwendungsprogrammen eine Rolle spielen. Dies wird am
Beispiel eines Semaphor-Mechanismus (vgl. S. 50) gezeigt.
Ein Semaphor-Mechanismus kann mit Hilfe von wait()
und notify() in Java realisiert werden. Abb. 5.11 zeigt
eine einfache Realisierung, vgl. auch [40, 52]. Die Metho-
de acquire() wartet (wenn notwendig), bis der interne
Z¨hler des Semaphor mindestens den Wert 1 angenommen
a
hat. Sobald dies der Fall ist, wird der Z¨hler dekremen-
a
tiert. Die Methode release() inkrementiert den Z¨hler a
und weckt mit notify() einen wartenden Thread auf, der
in acquire() durch den Aufruf von wait() blockiert wur-
de. Einen wartenden Thread kann es nur geben, wenn der
Z¨hler vor dem Inkrementieren den Wert 0 hatte, denn
a
nur dann wird ein Thread in acquire() blockiert. Da der
Z¨hler nur um 1 inkrementiert wurde, reicht es aus, einen
a
wartenden Thread aufzuwecken. Die Alternative w¨re der
a
Einsatz von notifyAll(), wodurch alle wartenden Thre-
ads aufgeweckt w¨ rden. Von diesen k¨nnte aber nur einer
u o
den Z¨hler dekrementieren. Da danach der Z¨hler wieder
a a
den Wert 0 hat, w¨ rden alle anderen Threads durch den
u
Aufruf von wait wieder blockiert.
Der in Abb. 5.11 beschriebene Semaphor-Mechanismus
kann f¨ r die Synchronisation eines Produzenten-Konsu-
u
menten-Verh¨ltnisses zwischen Threads verwendet werden.
a
Ein ¨hnlicher Mechanismus wurde in Abb. 5.8 direkt mit
a
wait() und notify() realisiert. Abb. 5.13 zeigt eine al-
ternative Realisierung mit Semaphoren, vgl. auch [40]. Der
Produzent legt die von ihm erzeugten Objekte in einem
Puffer fester Gr¨ße ab, der Konsument entnimmt Objekte
o
110 5 Java-Threads
public class Semaphore {
private long counter;
public Semaphore(long init) {
counter = init;
}
public void acquire()
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
synchronized (this) {
try {
while (counter <= 0) wait();
counter--;
}
catch (InterruptedException ie) {
notify(); throw ie;
}
}
}
public synchronized void release() {
counter++;
notify();
}
}
Abbildung 5.11. Realisierung eines Semaphor-Mechanismus.
aus dem Puffer und verarbeitet sie weiter. Der Produzent
kann nur Objekte im Puffer ablegen, wenn dieser nicht voll
ist, der Konsument kann nur Objekte entnehmen, wenn
der Puffer nicht leer ist. Die eigentliche Pufferverwaltung
wird durch eine separate Klasse BufferArray realisiert, die
Methoden insert() bzw. extract() zum Einf¨ gen bzw.
u
Entnehmen von Objekten zur Verf¨ gung stellt, vgl. Abb.
u
5.4 Erweiterte Synchronisationsmuster 111
5.12. Beide Methoden sind synchronized, so dass mehre-
re Threads konkurrierend auf Objekte der Klasse zugreifen
k¨nnen. Ein Mechanismus zur Kontrolle eines eventuellen
o
Puffer¨ berlaufs ist nicht enthalten.
u
public class BufferArray {
private final Object[] array;
private int putptr = 0;
private int takeptr = 0;
public BufferArray (int n) {
array = new Object[n];
}
public synchronized void insert (Object obj) {
array[putptr] = obj;
putptr = (putptr +1) % array.length;
}
public synchronized Object extract() {
Object x = array[takeptr];
array[takeptr] = null;
takeptr = (takeptr +1) % array.length;
return x;
}
}
Abbildung 5.12. Klasse BufferArray zur Pufferverwaltung.
Die Klasse BoundedBufferSema in Abb. 5.13 stellt Me-
thoden put() und take() zur Ablage bzw. Entnahme
eines Objektes im Puffer zur Verf¨ gung. Zur Kontrolle
u
des Puffers werden zwei Semaphore putPermits() und
takePermits() verwendet, die zu jedem Zeitpunkt die
erlaubte Anzahl von Ablagen (Produzent) bzw. Entnah-
men (Konsument) angeben; putPermits() wird mit der
Puffergr¨ße, takePermits() mit 0 initialisiert. Beim Ab-
o
112 5 Java-Threads
pulic class BoundedBufferSema {
private final BufferArray buff;
private final Semaphore putPermits;
private final Semaphore takePermits;
public BoundedBufferSema(int capacity)
throws IllegalArgumentException {
if (capacity <= 0)
throw new IllegalArgumentException();
buff = new BufferArray(capacity);
putPermits = new Semaphore(capacity);
takePermits = new Semaphore(0);
}
public void put(Object x)
throws InterruptedException {
putPermits.acquire();
buff.insert(x);
takePermits.release();
}
public Object take()
throws InterruptedException {
takePermits.acquire();
Object x = buff.extract();
putPermits.release();
return x;
}
}
Abbildung 5.13. Pufferverwaltung mit Semaphoren.
legen eines Objektes mittels put() wird der Semaphor
putPermits() mit acquire() dekrementiert; bei vollem
Puffer wird der ablegende Thread dabei eventuell blockiert.
Nach Ablage eines Objektes mit insert() wird ein even-
tuell wartender Konsumenten-Thread mittels release()
bzgl. dem Semaphor takePermits() aufgeweckt. Die Ent-
5.5 Thread-Scheduling in Java 113
nahme eines Objektes mittels take() arbeitet analog mit
vertauschter Rolle der Semaphoren.
Im Vergleich zur Realisierung aus Abb. 5.8 verwendet
die Semaphor-Implementierung aus Abb. 5.13 zwei separa-
te Objekte (vom Typ Semaphore) zur Kontrolle des Puffer-
status. Dies kann je nach Situation zu einer Reduktion des
Synchronisationsaufwands f¨ hren: Bei der Implementierung
u
aus Abb. 5.8 werden in put() bzw. take() alle warten-
den Threads aufgeweckt. Von diesen kann jedoch nur einer
weiterarbeiten, indem er ein abgelegtes Objekt entnimmt
oder einen frei werdenden Eintrag zur Ablage eines Objek-
tes nutzt. Alle anderen Threads werden erneut blockiert.
Bei der Implementierung aus Abb. 5.13 wird hingegen nur
ein Thread aufgeweckt, der darauf wartet, dass der Puffer
nicht mehr leer bzw. nicht mehr voll ist.
5.5 Thread-Scheduling in Java
Ein Java-Programm besteht typischerweise aus mehreren
Threads, die auf einem oder mehreren Prozessoren aus-
gef¨ hrt werden. Die ausf¨ hrungsbereiten Threads konkur-
u u
rieren dabei um die Ausf¨ hrung auf einem freiwerdenden
u
Prozessor. Die jeweilige Zuordnung von Threads an Prozes-
soren wird vom Scheduler der JVM durchgef¨hrt. Der Pro-
u
grammierer kann die Zuordnung von Threads an Prozesso-
ren dadurch beeinflussen, dass er Threads Priorit¨ten zu-
a
ordnet. Die minimalen, maximalen und Default-Priorit¨ten
a
von Java-Threads sind in statischen Konstanten der Klasse
Thread festgelegt:
public static final int MIN PRIORITY // Default 1
public static final int MAX PRIORITY // Default 10
public static final int NORM PRIORITY // Default 5
114 5 Java-Threads
Dabei entspricht ein großer Priorit¨tswert einer hohen Prio-
a
rit¨t. Der die main()-Methode einer Klasse ausf¨ hrende
a u
Hauptthread hat per Default die Priorit¨t Thread.NORM -
a
PRIORITY. Ein neu erzeugter Thread hat per Default die
gleiche Priorit¨t wie der erzeugende Thread. Die aktuelle
a
Priorit¨t eines Threads kann mit Hilfe der Methoden
a
public int getPriority()
public int setPriority(int prio)
abgefragt bzw. dynamisch ge¨ndert werden.
a
Gibt es mehr ausf¨ hrungsbereite Threads als Prozesso-
u
ren, bringt der Scheduler vorzugsweise Threads mit einer
h¨heren Priorit¨t zur Ausf¨ hrung. Der exakte Mechanis-
o a u
mus zur Auswahl der auszuf¨ hrenden Threads kann von der
u
speziellen Implementierung der JVM abh¨ngen. Die Pro-
a
grammiersprache Java legt keinen genauen Mechanismus
f¨ r das Scheduling fest, um die Flexibilit¨t der Realisie-
u a
rung der JVM auf verschiedenen Plattformen und Betriebs-
systemen nicht zu beeintr¨chtigen. Der Scheduler kann im-
a
mer den Thread mit der h¨chsten Priorit¨t zur Ausf¨ hrung
o a u
bringen, er kann aber auch einen Alterungsmechanismus in-
tegrieren, der sicherstellt, dass auch Threads mit geringe-
rer Priorit¨t ab und zu zur Ausf¨ hrung kommen. Da das
a u
genaue Scheduling von Threads unterschiedlicher Priorit¨ta
nicht festgelegt ist, k¨nnen Priorit¨ten nicht dazu verwen-
o a
det werden, Synchronisationsmechanismen zu ersetzen.
Bei Verwendung von Threads mit unterschiedlichen Prio-
rit¨ten kann das Problem der Priorit¨tsinversion auftre-
a a
ten: Eine Priorit¨tsinversion tritt auf, wenn ein Thread
a
hoher Priorit¨t blockiert und auf einen Thread niedriger
a
Priorit¨t wartet, weil dieser z.B. eine Mutexvariable ge-
a
sperrt hat. Der Thread niedriger Priorit¨t kann aber von
a
einem Thread mittlerer Priorit¨t am Weiterarbeiten und an
a
der Freigabe der Mutexvariable gehindert werden mit dem
5.6 Paket java.util.concurrent 115
Effekt, dass der Thread hoher Priorit¨t m¨glicherweise lan-
ao
ge Zeit blockiert. Das Problem der Priorit¨tsinversion kann
a
durch Verwendung von Priorit¨tsvererbung gel¨st werden:
a o
wenn ein Thread hoher Priorit¨t blockiert, wird die Prio-
a
rit¨t des Threads, der das kritische Objekt zur Zeit kon-
a
trolliert auf die Priorit¨t des Threads hoher Priorit¨t an-
a a
gehoben. Damit kann kein Thread mittlerer Priorit¨t den
a
Thread hoher Priorit¨t vom Weiterarbeiten abhalten. Viele
a
JVM setzen daher diese Methode ein; dies ist jedoch nicht
vom Java-Standard festgelegt.
5.6 Paket java.util.concurrent
Ab der Java2 Platform (Java 2 Standard Edition 5.0,
J2SE5.0) stehen durch das Paket java.util.concurrent
zus¨tzliche Synchronisationsmechanismen zur Verf¨ gung,
a u
die auf den bisher besprochenen Mechanismen, also syn-
chronized-Bl¨cke, wait() und notify(), aufbauen. Die
o
zus¨tzlichen Mechanismen stellen abstraktere und flexible-
a
re Synchronisationsoperationen zur Verf¨ gung. Diese bein-
u
halten u.a. atomare Variablen, Sperrvariablen, Barrier-Syn-
chronisation, Bedingungsvariablen und Semaphore sowie
verschiedene threadsichere Datenstrukturen. Die zus¨tzli-
a
chen Klassen sind ¨hnlich zu den in [40] besprochenen Klas-
a
¨
sen. Wir geben im Folgenden einen kurzen Uberblick und
verweisen f¨ r eine detailliertere Behandlung auf [25].
u
Semaphor-Mechanismus
Die Klasse Semaphore stellt einen Semaphor-Mechanismus
a
¨hnlich zu Abb. 5.11 zur Verf¨ gung. Intern enth¨lt die
u a
Klasse einen Z¨hler, der die Anzahl der Zugriffserlaubnisse
a
z¨hlt. Die wichtigsten Methoden der Klasse sind:
a
116 5 Java-Threads
void acquire();
void release();
boolean tryAcquire()
boolean tryAcquire(int permits, long timeout,
TimeUnit unit)
Die Methode acquire() erfragt eine Zugriffserlaubnis und
blockiert, falls keine vorhanden ist. Ist eine Zugriffserlaub-
nis vorhanden, wird die Anzahl der vorhandenen Zugriffs-
erlaubnisse dekrementiert und die Kontrolle wird direkt
wieder dem aufrufenden Thread ubergeben. Die Methode
¨
release() f¨ gt eine Zugriffserlaubnis zum Semaphor hin-
u
zu. Wartet zu diesem Zeitpunkt ein anderer Thread auf
eine Zugriffserlaubnis, wird er aufgeweckt. Die Methode
tryAcquire() versucht, eine Zugriffserlaubnis zu erhalten.
Ist dies erfolgreich, wird true zur¨ ckgeliefert. Ist dies nicht
u
erfolgreich, wird false zur¨ ckgeliefert; im Unterschied zu
u
acquire() erfolgt also keine Blockierung des ausf¨ hrenden
u
Threads. Die Methode tryAcquire() mit Parametern er-
laubt die zus¨tzliche Angabe einer Anzahl von Zugriffser-
a
laubnissen (permits) und einer Wartezeit (timeout) mit ei-
ner Zeiteinheit (unit). Sind nicht gen¨ gend Zugriffserlaub-
u
nisse verf¨ gbar, wird der ausf¨ hrende Thread blockiert, bis
u u
eine der folgenden Bedingungen eintritt:
• die Anzahl der angefragten Zugriffserlaubnisse wird ver-
f¨ gbar, indem andere Threads release() ausf¨ hren
u u
(R¨ ckgabewert true);
u
• die angegebene Wartezeit ist abgelaufen (R¨ ckgabewert
u
false);
Barrier-Synchronisation
Die Klasse CyclicBarrier aus java.util.concurrent lie-
fert einen Barrier-Synchronisationsmechanismus, wobei sich
5.6 Paket java.util.concurrent 117
die Bezeichnung Cyclic darauf bezieht, dass ein Objekt der
Klasse wiederverwendet werden kann, wenn alle Threads
die Barrier passiert haben. Die Konstruktoren der Klasse
public CyclicBarrier (int n)
public CyclicBarrier (int n, Runnable action)
erlauben die Angabe der Anzahl n von Threads, die die
Barrier passieren m¨ ssen sowie die Angabe einer Aktion
u
action, die ausgef¨ hrt wird, sobald alle Threads die Bar-
u
rier passiert haben. Durch Aufruf der Methode await()
wartet ein Thread an der Barrier, bis die angegebene An-
zahl von Threads die Barrier erreicht haben. Durch Aufruf
der Methode reset() wird ein Barrierobjekt wieder in den
urspr¨ nglichen Zustand zur¨ ckgesetzt.
u u
Sperrmechanismus
Das Paket java.util.concurrent.locks enth¨lt Interfa-
a
ces und Klassen f¨ r Sperren und das Warten auf das Ein-
u
treten von Bedingungen.
Das Interface Lock definiert uber synchronized-Bl¨cke
o
¨
und -Methoden hinausgehende Sperrmechanismen, die nicht
nur auf eine Synchronisation bzgl. der impliziten Mutexva-
riablen der jeweiligen Objekte beschr¨nkt sind. Die wich-
a
tigsten definierten Methoden sind:
void lock()
boolean tryLock()
boolean tryLock(long time, TimeUnit unit)
void unlock()
Die Methode lock() f¨ hrt einen Sperrversuch durch. Ist
u
die Sperre bereits von einem anderen Thread gesetzt, wird
der ausf¨ hrende Thread blockiert, bis der andere Thread
u
118 5 Java-Threads
ihn mit unlock() wieder aufweckt. Ist die Sperre nicht ge-
setzt, wird der ausf¨ hrende Thread Eigent¨ mer der Sper-
u u
re. Die Methode tryLock() f¨ hrt ebenfalls einen Sperrver-
u
such durch. Bei Erfolg wird true als R¨ ckgabewert zur¨ ck-
u u
geliefert. Bei Misserfolg wird false zur¨ ckgeliefert, der
u
ausf¨ hrende Thread wird aber nicht blockiert. Die Methode
u
tryLock() mit Parametern erlaubt die zus¨tzliche Anga-
a
be einer Wartezeit analog zu tryAcquire(). Die Methode
unlock() gibt eine vorher gesetzte Sperre wieder frei. Da-
bei wird ein auf die Sperre wartender Thread aufgeweckt.
Eine Realisierung des Interface Lock wird durch die
Klasse ReentrantLock zur Verf¨ gung gestellt. Der Kon-
u
struktor der Klasse erlaubt die Angabe eines optionalen
Fairness-Parameters:
ReentrantLock()
ReentrantLock(boolean fairness)
Wird dieser auf true gesetzt, erh¨lt im Zweifelsfall der am
a
l¨ngsten wartende Thread Zugriff auf das Sperrobjekt. Oh-
a
ne Verwendung des Fairness-Parameters kann von keiner
speziellen Zugriffsreihenfolge ausgegangen werden. Die Ver-
wendung des Fairness-Parameters kann zu einem erh¨hteno
Verwaltungsaufwand und dadurch verringertem Durchsatz
f¨ hren. Eine typische Benutzung der Klasse ReentrantLock
u
ist in Abb. 5.14 skizziert.
Signalmechanismus
Das Interface Condition aus java.util.concurrent.lock
spezifiziert einen Signalmechanismus mit Bedingungsvaria-
blen, so dass ein Thread auf das Eintreten einer Bedin-
gung warten kann, deren Eintreten ihm durch ein Signal
eines anderen Threads mitgeteilt wird, wie dies auch in
Pthreads durchgef¨ hrt wird (vgl. S. 70). Eine Bedingungs-
u
variable wird immer an eine Sperrvariable (vgl. Interface
5.6 Paket java.util.concurrent 119
import java.util.concurrent.locks.*;
pulic class NewClass {
private ReentrantLock lock = new ReentrantLock();
//...
public void method() {
lock.lock();
try {
//...
} finally { lock.unlock(); }
}
}
Abbildung 5.14. Illustration der Verwendung von
ReentrantLock-Objekten.
Lock) gebunden. Eine Bedingungsvariable zu einer Sperr-
variable kann mit der Methode
Condition newCondition()
von Objekten, die das Interface Lock implementieren, er-
zeugt werden. Die zur¨ ckgelieferte Bedingungsvariable ist
u
fest an die Sperrvariable gebunden, bzgl. der die Methode
newCondition() aufgerufen wird. Auf eine Bedingungsva-
riable k¨nnen die folgenden Methoden angewendet werden:
o
void await()
void await(long time, TimeUnit unit)
void signal()
void signalAll()
Die Methode await() blockiert den ausf¨ hrenden Thread,
u
bis er von einem anderen Thread wieder mit einem Signal
aufgeweckt wird. Gleichzeitig wird die zugeh¨rige Sperrva-
o
riable atomar freigegeben. Vor dem Aufruf von await()
120 5 Java-Threads
muss der ausf¨ hrende Thread also die zugeh¨rige Sperr-
u o
variable erfolgreich gesperrt haben. Nach dem Aufwecken
durch ein Signal eines anderen Threads muss der vorher blo-
ckierte Thread zuerst wieder die Kontrolle uber die Sperr-
¨
variable erhalten, bevor der Thread weiterarbeiten kann.
Wird await() mit Parametern verwendet, wird der Thread
nach Ablauf der angegebenen Wartezeit aufgeweckt, auch
wenn noch kein Signal eines anderen Threads eingetroffen
ist.
Mit signal() kann ein Thread einen bzgl. einer Be-
dingungsvariable wartenden Thread wieder aufwecken. Mit
signalAll() werden alle bzgl. der Bedingungsvariable war-
tenden Threads aufgeweckt.
Die Verwendung von Bedingungsvariablen f¨ r die Reali-
u
sierung eines Puffermechanismus ist in Abb. 5.15 illustriert.
Die Bedingungsvariablen werden ¨hnlich wie der Semaphor
a
in Abb. 5.13 verwendet.
Atomare Operationen
Das Paket java.util.concurrent.atomic stellt f¨ r ele-
u
mentare Datentypen atomare Operationen zur Verf¨ gung,
u
die einen sperrfreien Zugriff auf einzelne Variablen erlau-
ben. Ein Beispiel ist die Klasse AtomicInteger, die u.a.
die Methoden
boolean compareAndSet (int expect, int update)
int getAndIncrement()
enth¨lt. Die erste Methode setzt den Wert der Variablen auf
a
update, falls der Wert vorher expect war, und liefert true
bei erfolgreicher Ausf¨ hrung zur¨ ck. Die Operation erfolgt
u u
atomar, d.h. w¨hrend der Ausf¨ hrung kann der Thread
a u
nicht unterbrochen werden. Die zweite Methode inkremen-
tiert den Wert der Variablen atomar um 1 und liefert den
5.6 Paket java.util.concurrent 121
import java.util.concurrent.locks.*;
pulic class BoundedBufferCondition {
private Lock lock = new ReentrantLock();
private Condition notFull = lock.newCondition();
private Condition notEmpty = lock.newCondition();
private Object[] items = new Object[100];
private int putptr, takeptr, count;
public void put (Object x)
throws InterruptedException
lock.lock();
try {
while (count == items.length)
notFull.await();
items[putptr] = x;
putptr = (putptr +1) % items.length;
++count;
notEmpty.signal();
} finally { lock.unlock(); }
}
public Object take()
throws InterruptedException {
lock.lock();
try {
while (count == 0)
notEmpty.await();
Object x = items[takeptr];
takeptr = (takeptr +1) % items.length;
--count;
notFull.signal();
return x;
} finally {lock.unlock();}
}
}
Abbildung 5.15. Realisierung eines Puffermechanismus mit Hilfe
von Bedingungsvariablen.
122 5 Java-Threads
fr¨ heren Wert der Variablen als Ergebnis zur¨ ck. Die Klas-
u u
se stellt eine Vielzahl ¨hnlicher Methoden zur Verf¨ gung.
a u
Taskbasierte Ausf¨hrung von Programmen
u
Das Paket java.util.concurrent stellt auch einen Me-
chanismus f¨ r eine taskbasierte Formulierung von Program-
u
men bereit. Eine Task ist dabei eine durchzuf¨ hrende Be-
u
rechnungsfolge des Programms, die von einem beliebigen
Thread ausgef¨ hrt werden kann. Eine Abarbeitung von
u
Tasks wird durch das Interface Executor unterst¨ tzt:
u
public interface Executor {
void execute (Runnable command);
}
Dabei beschreibt command die auszuf¨ hrende Task, der
u
durch den Aufruf von execute() zur Ausf¨ hrung gebracht
u
wird. F¨ r Multicore-Prozessoren stehen dabei ublicherweise
u ¨
mehrere Threads zur Ausf¨ hrung von Tasks zur Verf¨ gung.
u u
Diese k¨nnen in einem Thread-Pool zusammengefasst wer-
o
den, wobei jeder Thread eine beliebige Task ausf¨ hren
u
kann. Im Vergleich zu einer Ausf¨ hrung jeder Task in ei-
u
nem eigenen Thread f¨ hrt der Einsatz von Thread-Pools
u
typischerweise zu einem geringeren Verwaltungsaufwand,
insbesondere wenn die Tasks wenige Berechnungen umfas-
sen. Zur Organisation von Thread-Pools kann die Klasse
Executors eingesetzt werden, die Methoden zur Erzeugung
und Verwaltung von Thread-Pools bereitstellt. Die wich-
tigsten sind
static ExecutorService newFixedThreadPool(int n)
static ExecutorService newCachedThreadPool()
static ExecutorService newSingleThreadExecutor()
5.6 Paket java.util.concurrent 123
Die erste Methode erzeugt einen Thread-Pool, der neue
Threads bei Einf¨ gen von Tasks startet, bis die angegebe-
u
ne maximale Anzahl n von Threads erreicht ist. Die zweite
Methode erzeugt einen Thread-Pool, bei dem die Anzahl
der Threads dynamisch an die Anzahl der Tasks angepasst
wird, wobei Threads wieder terminiert werden, wenn sie f¨ r
u
eine bestimmte Zeit (60 Sekunden) nicht genutzt werden.
Die letzte Methode erzeugt einen einzelnen Thread, der eine
Menge von Tasks abarbeitet.
Zur Unterst¨ tzung der Abarbeitung taskbasierter An-
u
wendungen definiert das von Executor abgeleitete Inter-
face ExecutorService u.a. Methoden zur Terminierung
von Thread-Pools. Die wichtigsten dieser Methoden sind:
void shutdown();
List<Runnable> shutdownNow();
Die Methode shutdown() bewirkt, dass der Thread-Pool
keine weiteren Tasks mehr annimmt; die bereits enthal-
tenen Tasks werden aber noch ausgef¨hrt. Die Metho-
u
de shutdownNow() stoppt zus¨tzlich alle im Moment aus-
a
gef¨ hrten Tasks; wartende Tasks werden nicht mehr aus-
u
gef¨ hrt. Die Liste der wartenden Tasks wird als R¨ ck-
u u
gabewert zur¨ ckgeliefert. Die Klasse ThreadPoolExecutor
u
stellt eine Realisierung des Interfaces ExecutorService zur
Verf¨ gung.
u
Abb. 5.16 illustriert die Verwendung eines Thread-Pools
am Beispiel eines Webservers, der uber einen ServerSocket
¨
auf Verbindungsanfragen von Clients wartet und diese als
Tasks mit execute() von den Threads eines Thread-Pools
bearbeiten l¨ßt. Jede abzuarbeitende Task wird als Ob-
a
jekt vom Typ Runnable erzeugt und spezifiziert die durch-
zuf¨ hrende Berechnung handleRequest() als run()-Me-
u
thode. Die Gr¨ße des Thread-Pools ist auf 10 Threads be-
o
grenzt.
124 5 Java-Threads
import java.io.IOException;
import java.net.*;
import java.util.concurrent.*;
pulic class TaskWebServer {
static class RunTask implements Runnable {
private Socket myconnection;
public RunTask (Socket connection) {
myconnection = connection;
}
public void run() {
// handleRequest(myconnection);
}
}
public static void main (String[] args)
throws IOException {
ServerSocket s = new ServerSocket(80);
ExecutorService pool =
Executors.newFixedThreadPool(10);
try {
while (true) {
Socket connection = s.accept();
Runnable task = new RunTask(connection)
pool.execute(task);
}
} catch (IOException ex) {
pool.shutdown();
}
}
}
Abbildung 5.16. Skizze eines taskbasierten Webservers.
6
OpenMP
¨
OpenMP ist eine Spezifikation von Ubersetzerdirektiven,
Bibliotheksfunktionen und Umgebungsvariablen, die von ei-
ner Gruppe von Soft- und Hardwareherstellern mit dem
Ziel entworfen wurde, einen einheitlichen Standard f¨ r die
u
Programmierung von Parallelrechnern mit gemeinsamem
Adressraum zur Verf¨ gung zu stellen [53]. Unterst¨ tzt wer-
u u
den Schnittstellen f¨ r C, C++ und FORTRAN. OpenMP
u
erweitert diese sequentiellen Sprachen um Konstrukte zur
SPMD-Programmierung, zur Aufteilung von Arbeit, zur
Synchronisation und zur Deklaration von gemeinsamen
(shared) und privaten (private) Variablen. Die Auswahl
der Konstrukte ist auf den Anwendungsbereich des wissen-
schaftlichen Rechnens ausgerichtet. Es k¨nnen aber auch
o
andere Anwendungen in OpenMP realisiert werden.
6.1 Programmiermodell
Das Programmiermodell von OpenMP basiert auf parallel
arbeitenden Threads, die nach einem fork-join-Prinzip
126 6 OpenMP
erzeugt und beendet werden. Die Abarbeitung eines mit
Hilfe von OpenMP formulierten Programms beginnt mit
der Ausf¨ hrung eines Master-Threads, der das Programm
u
sequentiell ausf¨ hrt, bis das erste parallel-Konstrukt auf-
u
tritt. Bei Auftreten dieses Konstrukts, das weiter unten
n¨her beschrieben wird, erzeugt der Master-Thread ein
a
Team von Threads und wird zum Master des Teams (fork).
Alle Threads des Teams, zu dem auch der Master selber
geh¨rt, f¨ hren das auf das parallel-Konstrukt folgende
o u
Programmst¨ ck parallel zueinander aus, indem entweder
u
alle Threads des Teams den gleichen Programmtext mit
evtl. unterschiedlichen privaten Variablen im SPMD-Stil
abarbeiten oder indem die Arbeit explizit durch geeigne-
te Konstrukte auf die Threads verteilt wird. Dabei wird
ein gemeinsamer Adressraum f¨ r das Gesamtprogramm zu-
u
grundegelegt, d.h. wenn ein Mitglied des Teams eine Daten-
¨
struktur ¨ndert, ist die Anderung nicht nur f¨ r die anderen
a u
Mitglieder des Teams, sondern auch f¨ r alle anderen Thre-
u
ads des Programms sichtbar. Nach Beendigung der Abar-
beitung des parallel auszuf¨ hrenden Programmst¨ ckes wer-
u u
den die Threads des Teams synchronisiert und nur der Mas-
ter des Teams wird weiter ausgef¨ hrt; die anderen Threads
u
werden beendet (join).
Mit den zur Verf¨ gung stehenden Mechanismen zur
u
Steuerung der Parallelit¨t k¨nnen Programme formuliert
ao
werden, die sowohl sequentiell als auch parallel ausgef¨ hrt
u
werden k¨nnen. Dabei ist es jedoch auch m¨glich, Program-
o o
me zu schreiben, die nur bei einer parallelen Ausf¨ hrung
u
das gew¨ nschte Ergebnis errechnen. Der Programmierer ist
u
daf¨ r verantwortlich, dass die Programme korrekt arbeiten.
u
Dies gilt auch f¨ r die Vermeidung von Konflikten, Dead-
u
locks oder zeitkritischen Abl¨ufen.
a
Die meisten Mechanismen zur Steuerung der paralle-
len Abarbeitung von Programmteilen werden in OpenMP
6.2 Spezifikation der Parallelit¨t
a 127
¨
durch Ubersetzer-Direktiven zur Verf¨ gung gestellt, deren
u
Syntax auf den in C und C++ verwendeten #pragma-
Direktiven basiert. Zus¨tzlich stehen Laufzeitfunktionen
a
zur Steuerung des Verhaltens der Direktiven zur Verf¨ gung.
u
Jede Direktive wirkt nur auf die der Direktive direkt folgen-
de Anweisung. Sollen mehrere Anweisungen von der Direk-
tive gesteuert werden, so m¨ ssen diese zwischen { und }
u
stehen und so in einem Anweisungsblock zusammengefasst
sein. F¨ r OpenMP-Programme muss die Datei <omp.h> ein-
u
gebunden werden.
¨
Der Rest des Kapitels enth¨lt einen kurzen Uberblick
a
uber OpenMP. Weiterf¨ hrende Informationen k¨nnen in
u o
¨
[13, 53, 59] nachgelesen und uber die OpenMP-Webseite
¨
(http://www.openmp.org) erhalten werden.
6.2 Spezifikation der Parallelit¨t
a
Die wichtigste Direktive zur Steuerung der Parallelit¨t ist
a
die parallel-Direktive mit der Syntax:
#pragma omp parallel [Parameter [Parameter] ... ]
Anweisungsblock
Diese Direktive bewirkt, dass der angegebene Anwei-
sungsblock parallel ausgef¨ hrt wird. Wird die Arbeit nicht
u
explizit verteilt, so f¨ hren alle Threads die gleichen Be-
u
rechnungen mit evtl. unterschiedlichen privaten Daten im
SPMD-Stil aus. Der parallel ausgef¨ hrte Anweisungsblock
u
wird auch als paralleler Bereich bezeichnet. Zur paralle-
len Abarbeitung wird ein Team von Threads erzeugt, dessen
Master der die Direktive ausf¨ hrende Thread ist. Die ge-
u
naue Anzahl der zu erzeugenden Threads kann uber Lauf-
¨
zeitfunktionen oder Umgebungsvariablen beeinflusst wer-
den. Nach der Erzeugung des Teams bleibt die Anzahl der
128 6 OpenMP
Threads, die den Anweisungsblock ausf¨hren, konstant. F¨ r
u u
verschiedene parallele Bereiche k¨nnen jedoch verschiedene
o
Thread-Anzahlen verwendet werden.
Ein paralleler Bereich wird von allen Threads des er-
zeugten Teams einschließlich des Master-Threads gemein-
sam abgearbeitet. Dabei k¨nnen gemeinsame und private
o
Variablen der beteiligten Threads uber die Parameter der
¨
parallel-Direktive definiert werden. Private Variablen der
Threads werden durch den private-Parameter der Form
private(list of variables)
spezifiziert, wobei list of variables eine beliebige Liste
von bereits deklarierten Programmvariablen ist. Der Effekt
besteht darin, dass auf dem Laufzeitstack jedes Threads des
Teams eine uninitialisierte Kopie der angegebenen Varia-
blen angelegt wird, die nur dieser Thread w¨hrend seiner
a
Ausf¨ hrung als globale Variable zugreifen und ver¨ndern
u a
kann. Gemeinsame Variablen der Threads eines Teams wer-
den durch den shared-Parameter der Form
shared(list of variables)
spezifiziert. Der Effekt besteht darin, dass jeder Thread des
Teams beim Lesen oder Beschreiben der angegebenen Va-
riablen auf denselben Datenbereich zugreift.
Mit Hilfe des default-Parameters kann der Program-
mierer festlegen, ob die Programmvariablen des parallel-
Konstrukts per Default gemeinsame oder private Variablen
sind. Die Angabe
default(shared)
bewirkt, dass alle außer den vom private-Parameter expli-
zit angegebenen Programmvariablen gemeinsame Variablen
der Threads des Teams sind. Die Angabe
6.2 Spezifikation der Parallelit¨t
a 129
default(none)
bewirkt, dass jede in dem parallelen Bereich verwendete Va-
riable explizit uber einen shared- oder private-Parameter
¨
als gemeinsame oder private Variable gekennzeichnet sein
muss.
Das Programmfragment in Abbildung 6.1 zeigt die Ver-
wendung einer parallel-Direktive zur parallelen Verarbei-
tung eines Feldes x. Wir nehmen an, dass die zu verarbei-
tenden Werte in der Funktion initialize() vom Master-
Thread eingelesen werden. In der parallel-Direktive wer-
den die Variablen x und npoints als gemeinsame Variable
der den parallelen Bereich ausf¨ hrenden Threads spezifi-
u
ziert. Die restlichen Variablen iam, np und mypoints sind
private Variablen. Zu Beginn der Ausf¨ hrung des paralle-
u
len Bereiches bestimmt jeder beteiligte Thread durch den
Aufruf der Funktion np = omp get num threads() die Ge-
samtanzahl der Threads des Teams. Durch den Aufruf der
Funktion omp get thread num() erh¨lt jeder Thread des
a
Teams eine Nummer zur¨ ck, die als Thread-Name dient
u
und im Beispiel in iam gespeichert wird. Der Master-Thread
hat die Thread-Nummer 0, die Thread-Nummern der an-
deren Threads liegen fortlaufend zwischen 1 und np-1. Je-
der der Threads ruft die Funktion compute subdomain()
auf, in der die Eintr¨ge des Feldes x verarbeitet werden.
a
Beim Aufruf von compute subdomain() wird neben dem
Feldnamen x and dem Thread-Namen iam auch die Anzahl
mypoints der von jedem Thread zu verarbeitenden Feldele-
mente angegeben. Welche Feldelemente dies speziell sind,
ist innerhalb von compute subdomain anzugeben.
Nach Abarbeitung eines parallelen Bereiches werden al-
le Threads des Teams außer dem Master-Thread termi-
niert. Anschließend f¨ hrt der Master-Thread die dem par-
u
allelen Bereich folgenden Anweisungen alleine aus. Das En-
130 6 OpenMP
#include <stdio.h>
#include <omp.h>
int npoints, iam, np, mypoints;
double *x;
int main() {
scanf(\"%d\", &npoints);
x = (double *) malloc(npoints * sizeof(double));
initialize();
#pragma omp parallel shared(x,npoints)
private(iam,np,mypoints)
{
np = omp get num threads();
iam = omp get thread num();
mypoints = npoints / np;
compute subdomain(x, iam, mypoints);
}
}
Abbildung 6.1. Parallele Verarbeitung einer Datenstruktur mit
Hilfe einer OpenMP parallel-Direktive.
de eines parallelen Bereiches stellt somit einen impliziten
Synchronisationspunkt dar.
Prinzipiell k¨nnen parallele Bereiche geschachtelt wer-
o
den, d.h. in einem parallelen Bereich kann eine weitere
parallel-Direktive auftreten. Per Default wird der inne-
re parallele Bereich von einem Team ausgef¨ hrt, dem nur
u
der Thread angeh¨rt, der die innere parallel-Direktive
o
ausf¨ hrt. Dies kann durch Aufruf der Bibliotheksfunktion
u
void omp set nested(int nested)
mit nested != 0 ge¨ndert werden. In diesem Fall kann der
a
die geschachtelte parallel-Direktive ausf¨ hrende Thread
u
6.2 Spezifikation der Parallelit¨t
a 131
ein Team mit mehr als einem Thread erzeugen. Die genaue
Anzahl der in diesem Fall erzeugten Threads ist implemen-
tierungsabh¨ngig.
a
Parallele Schleife
Innerhalb eines parallelen Bereiches k¨nnen die durch-
o
zuf¨ hrenden Berechnungen mit Hilfe von speziellen Direkti-
u
ven zur Verteilung der Arbeit auf die ausf¨ hrenden Threads
u
verteilt werden. Die wichtigste Direktive zur Verteilung der
Arbeit ist die for-Direktive mit der folgenden Syntax:
#pragma omp for [Parameter [Parameter] ... ]
for (i = lower bound; i op upper bound; incr expr) {
Schleifenrumpf
}
Die for-Schleife ist auf solche Schleifen beschr¨nkt, bei de-
a
nen sichergestellt ist, dass die durch den Schleifenrumpf
gegebenen Berechnungen der verschiedenen Iterationen un-
abh¨ngig voneinander sind und die Gesamtzahl der Itera-
a
tionen beim Betreten der for-Schleife im voraus bestimmt
werden kann. Der Effekt der for-Direktive besteht dar-
in, dass die einzelnen Iterationen der Schleife auf die den
umgebenden parallelen Bereich ausf¨ hrenden Threads ver-
u
teilt und unabh¨ngig berechnet werden. Es soll sich al-
a
so um eine parallele Schleife fester L¨nge handeln. Die
a
Variable i bezeichnet eine Integervariable, die im Rumpf
der Schleife nicht ver¨ndert werden darf und die inner-
a
halb der Schleife als private Variable des die zugeh¨rige
o
Iteration der for-Schleife ausf¨ hrenden Threads behan-
u
delt wird. lower bound und upper bound bezeichnen Inte-
gerausdr¨ cke, deren Werte durch Ausf¨ hrung der Schleife
u u
nicht ge¨ndert werden, op bezeichnet einen Vergleichsope-
a
rator, also op ∈ { <, <=, >, >= }. Der Inkrementierungs-
Ausdruck incr expr kann folgende Formen annehmen:
132 6 OpenMP
++i, i++, --i, i--, i += incr, i -= incr,
i = i + incr, i = incr + i, i = i - incr,
wobei incr ebenfalls ein schleifenunabh¨ngiger Integeraus-
a
druck ist.
Die Aufteilung der Schleifeniterationen auf die
ausf¨ hrenden Threads kann durch den schedule-
u
Parameter gesteuert werden. Folgende Steuerungsm¨glich-
o
keiten sind vorgesehen:
• schedule(static, block size). Diese Parameter-
Angabe bedeutet, dass eine statische Aufteilung der Ite-
rationen auf die Threads verwendet wird, indem die
Iterationen in Bl¨cken der Gr¨ße block size reihum
o o
(round-robin) auf die Threads verteilt werden. Ist kei-
ne Blockgr¨ße angegeben, so erh¨lt jeder Thread einen
o a
Block fortlaufender Iterationen ungef¨hr gleicher Gr¨ße,
a o
d.h. es wird eine blockweise Verteilung verwendet.
• schedule(dynamic, block size). Diese Parameter-
Angabe bedeutet, dass eine dynamische Zuteilung von
Iterationsbl¨cken an die Threads vorgenommen wird,
o
d.h. nach Abarbeitung der zugewiesenen Iterationen
erh¨lt ein Thread dynamisch einen neuen Block mit
a
block size Iterationen zugeteilt. Ist keine Blockgr¨ße
o
angegeben, werden dynamisch einzelne Iterationen zu-
geteilt, d.h. es wird die Blockgr¨ße 1 verwendet.
o
• schedule(guided, block size). Diese Parameter-
Angabe bedeutet, dass ein dynamisches Scheduling mit
abnehmender Blockgr¨ße verwendet wird. F¨ r die An-
o u
gabe block size = 1 wird jedem Thread, der seine zu-
gewiesenen Iterationen beendet hat, dynamisch ein neu-
er Block von Iterationen zugewiesen, dessen Gr¨ße sich
o
aus dem Quotient der noch nicht bearbeiteten Itera-
tionen und der Anzahl der Threads ergibt, so dass die
Blockgr¨ße der zugewiesenen Iterationen linear mit der
o
6.2 Spezifikation der Parallelit¨t
a 133
Anzahl der ausgef¨ hrten Iterationen abnimmt. F¨ r die
u u
Angabe block size = k mit k > 1 nimmt die Block-
gr¨ße exponentiell zu k ab, der letzte Block kann jedoch
o
eine kleinere Gr¨ße haben. Die Angabe block size gibt
o
also die minimale Blockgr¨ße an, die (bis auf die eben
o
erw¨hnte Ausnahme) gew¨hlt werden kann. Ist kein
a a
Wert f¨ r block size angegeben, wird als Defaultwert
u
1 verwendet.
• schedule(runtime). Diese Parameter-Angabe bedeu-
tet, dass das Scheduling der Threads zur Laufzeit des
Programms festgelegt wird. Dies kann dadurch gesche-
hen, dass vor dem Start des Programms die Umgebungs-
variable OMP SCHEDULE durch Angabe von Scheduling-
Typ und Blockgr¨ße gesetzt wird, also beispielsweise als
o
setenv OMP SCHEDULE \"dynamic, 4\"
setenv OMP SCHEDULE \"guided\"
Wird dabei keine Blockgr¨ße angegeben, wird der
o
Defaultwert verwendet. Außer f¨ r das statische Schedu-
u
ling (static) ist dies block size = 1. Wenn die Um-
gebungsvariable OMP SCHEDULE nicht gesetzt ist, h¨ngt
a
das verwendete Scheduling von der Implementierung der
OpenMP-Bibliothek ab.
Fehlt die Angabe eines schedule-Parameters bei der for-
Direktive, wird ein Default-Schedulingverfahren verwen-
det, das von der Implementierung der OpenMP-Bibliothek
abh¨ngt. Die einer for-Direktive zugeordnete parallele
a
Schleife darf nicht durch eine break-Anweisung beendet
werden. Am Ende der parallelen Schleife findet eine im-
plizite Synchronisation der beteiligten Threads statt, d.h.
die der parallelen Schleife folgenden Anweisungen werden
erst ausgef¨ hrt, wenn alle beteiligten Threads die parallele
u
Schleife beendet haben. Diese Synchronisation kann durch
134 6 OpenMP
#include <omp.h>
double MA[100][100], MB[100][100], MC[100][100];
int i, row, col, size = 100;
int main() {
read input(MA, MB);
#pragma omp parallel shared(MA,MB,MC,size)
{
#pragma omp for schedule(static)
for (row = 0; row < size; row++) {
for (col = 0; col < size; col++)
MC[row][col] = 0.0;
}
#pragma omp for schedule(static)
for (row = 0; row < size; row++) {
for (col = 0; col < size; col++)
for (i = 0; i < size; i++)
MC[row][col] += MA[row][i] * MB[i][col];
}
}
write output(MC);
}
Abbildung 6.2. OpenMP-Programm zur parallelen Berechnung
einer Matrix-Matrix-Multiplikation unter Verwendung eines paralle-
len Bereiches mit einem Anweisungsblock aus zwei aufeinanderfol-
genden parallelen Schleifen.
die Angabe eines nowait-Parameters in der Parameterliste
der for-Direktive vermieden werden.
Abbildung 6.2 zeigt als Beispiel f¨ r die Anwendung einer
u
for-Direktive eine Programmskizze zur Realisierung einer
Matrix-Matrix-Multiplikation zweier Matrizen MA und
MB in OpenMP. Der parallele Bereich des Programms be-
6.2 Spezifikation der Parallelit¨t
a 135
steht aus zwei Phasen, die durch eine implizite Synchronisa-
tion voneinander getrennt sind. In der ersten Phase wird die
Ergebnismatrix MC mit 0 initialisiert, in der zweiten Pha-
se wird die eigentliche Matrix-Multiplikation durchgef¨hrt.
u
Die Aufteilung der Berechnung der Ergebnismatrix auf die
auszuf¨ hrenden Threads erfolgt durch ein statisches Sche-
u
duling, wobei jeder Thread einen Block von Zeilen initiali-
siert bzw. berechnet. Da jeder Eintrag und damit auch je-
de Zeile der Ergebnismatrix gleichen Berechnungsaufwand
hat, ist ein solches statisches Scheduling sinnvoll. Bei der
Berechnung der Ergebnismatrix MC in der zweiten Phase be-
steht jeder Schleifenrumpf der durch die for-Direktive be-
zeichneten parallelen Schleife aus einer doppelten (sequen-
tiellen) Schleife, wobei die ¨ußere Schleife uber die Eintr¨ge
a a
¨
der jeweiligen Zeile l¨uft und die innere Schleife zur Berech-
a
nung der Multiplikation von Zeile und Spalte dient.
Das Schachteln von for-Direktiven innerhalb eines par-
allelen Bereiches ist nicht erlaubt. Zur Schachtelung par-
alleler Schleifen m¨ ssen also auch die parallelen Bereiche
u
so geschachtelt werden, dass in jedem parallelen Bereich
h¨chstens eine for-Direktive enthalten ist.
o
Nichtiterative parallele Bereiche
Eine nichtiterative Verteilung der innerhalb eines parallelen
Bereiches durchzuf¨ hrenden Berechnung kann durch Ver-
u
wendung einer sections-Direktive erfolgen, deren Syntax
wie folgt definiert ist:
#pragma omp sections [Parameter [Parameter] ... ]
{
[#pragma omp section]
Anweisungsblock
[#pragma omp section
Anweisungsblock
136 6 OpenMP
.
.
.
]
}
Innerhalb einer sections-Direktive werden durch
section-Direktiven Abschnitte bezeichnet, die unabh¨ngig
a
voneinander sind und daher parallel zueinander von ver-
schiedenen Threads abgearbeitet werden k¨nnen. Jeder Ab-
o
schnitt beginnt mit #pragma omp section und kann ein
beliebiger Anweisungsblock sein. F¨ r den ersten innerhalb
u
der sections-Direktive definierten Anweisungsblock kann
die Angabe der section-Direktive entfallen. Am Ende ei-
ner sections-Direktive findet eine implizite Synchronisa-
tion statt, die durch die Angabe eines nowait-Parameters
vermieden werden kann.
Syntaktische Abk¨rzungen
u
Zur Vereinfachung der Schreibweise f¨ hrt OpenMP
u
Abk¨ rzungen f¨ r parallele Bereiche ein, in denen nur eine
u u
einzelne for- bzw. sections-Direktive enthalten ist. F¨ ru
einen parallelen Bereich mit einer einzelnen for-Direktive
kann die folgende Abk¨ rzung verwendet werden:
u
#pragma omp parallel for [Parameter [Parameter] · · · ]
for(i = lower bound; i op upper bound; incr expr) {
Schleifenrumpf
}
Dabei sind als Parameter alle f¨ r die parallel- und f¨ r
u u
die for-Direktive zugelassenen Parameter erlaubt. Analog
kann als Abk¨ rzung f¨ r eine einzelne in einem paralle-
u u
len Bereich enthaltene sections-Direktive folgendes Kon-
strukt verwendet werden:
6.2 Spezifikation der Parallelit¨t
a 137
#pragma omp parallel sections [Parameter [Parameter]· · ·]
{
[#pragma omp section]
Anweisungsblock
[#pragma omp section
Anweisungsblock
.
.
.
]
}
Thread-Anzahl
Ein paralleler Bereich wird von einer Anzahl von Threads
ausgef¨ hrt. Der Programmierer hat die M¨glichkeit, diese
u o
Anzahl uber mehrere Laufzeitfunktionen zu beeinflussen.
¨
Mit Hilfe der Funktion
void omp set dynamic (int dynamic threads)
kann der Programmierer die Anpassung der Thread-
Anzahl durch das Laufzeitsystem beeinflussen, wobei die
Funktion außerhalb eines parallelen Bereiches aufgerufen
werden muss. F¨ r dynamic threads = 0 wird die dy-
u
namische Anpassung durch das Laufzeitsystem erlaubt,
d.h. das Laufzeitsystem kann die Anzahl der Threads,
die f¨ r nachfolgende parallele Bereiche verwendet wer-
u
den, an die Systemgegebenheiten anpassen. W¨hrend der
a
Ausf¨ hrung desselben parallelen Bereiches wird die Anzahl
u
der ausf¨ hrenden Threads aber stets konstant gehalten.
u
F¨ r dynamic threads = 0 wird die dynamische Anpas-
u
sung der Thread-Anzahl ausgeschaltet, d.h. das Laufzeit-
system verwendet f¨ r nachfolgende parallele Bereiche die
u
derzeit eingestellte Anzahl von Threads. Welche der beiden
Varianten den Default darstellt, h¨ngt von der speziellen
a
138 6 OpenMP
OpenMP-Bibliothek ab. Der Status der Thread-Anpassung
kann durch Aufruf der Funktion
int omp get dynamic (void)
abgefragt werden. Der Aufruf liefert 0 zur¨ ck, wenn keine
u
dynamische Anpassung vorgesehen ist. Ansonsten wird ein
Wert = 0 zur¨ ckgeliefert.
u
Der Programmierer kann durch Aufruf der Funktion
void omp set num threads (int num threads)
die Anzahl der Threads beeinflussen, die f¨ r die Ausf¨ hrung
u u
nachfolgender paralleler Bereiche verwendet werden. Auch
dieser Aufruf muss außerhalb eines parallelen Bereiches
stattfinden. Der genaue Effekt des Aufrufes h¨ngt davon ab,
a
ob die automatische Thread-Anpassung durch das Laufzeit-
system eingeschaltet ist oder nicht. Wenn die automatische
Anpassung eingeschaltet ist, gibt num threads die maxi-
male Anzahl von Threads an, die im Folgenden verwendet
wird. Wenn keine automatische Anpassung erlaubt ist, gibt
num threads die tats¨chliche Anzahl von Threads an, die
a
f¨ r alle nachfolgenden parallelen Bereiche verwendet wird.
u
Wie oben dargestellt wurde, erlaubt OpenMP geschach-
telte parallele Bereiche. Die Anzahl der zur Abarbeitung
verwendeten Threads eines geschachtelten parallelen Berei-
ches h¨ngt vom Laufzeitsystem ab, kann jedoch vom Pro-
a
grammierer durch Aufruf der Funktion
void omp set nested (int nested)
beeinflusst werden. F¨ r nested = 0 wird die Abarbeitung
u
des inneren parallelen Bereiches sequentialisiert und nur
von einem Thread vorgenommen. Dies ist auch die Default-
Einstellung. F¨ r nested = 0 wird eine geschachtelte paral-
u
lele Abarbeitung erlaubt, d.h. das Laufzeitsystem kann zur
6.3 Koordination von Threads 139
Abarbeitung des inneren parallelen Bereiches zus¨tzliche
a
Threads verwenden. Die genaue Abarbeitung h¨ngt auch
a
hier wieder vom Laufzeitsystem ab und kann auch aus der
Abarbeitung durch nur einen Thread erfolgen. Durch Auf-
ruf der Funktion
int omp get nested (void)
kann der aktuelle Status zur Behandlung von geschachtel-
ten parallelen Bereichen abgefragt werden.
6.3 Koordination von Threads
Ein paralleler Bereich wird in OpenMP-Programmen in der
Regel von mehreren Threads ausgef¨ hrt, deren Zugriff auf
u
gemeinsame Variablen koordiniert werden muss. Zur Koor-
dination stellt OpenMP mehrere Direktiven zur Verf¨ gung,
u
die innerhalb von parallelen Bereichen verwendet werden
k¨nnen. Kritische Bereiche, die zu jedem Zeitpunkt
o
nur von jeweils einem Thread ausgef¨ hrt werden sollten,
u
k¨nnen durch die critical-Direktive mit der folgenden
o
Syntax
#pragma omp critical [(name)]
Anweisungsblock
realisiert werden. Der optional anzugebende Name name
kann dabei zur Identifikation des kritischen Bereiches ver-
wendet werden. Der Effekt der critical-Direktive besteht
darin, dass ein Thread beim Erreichen der Direktive so lan-
ge wartet, bis kein anderer Thread den Anweisungsblock
des kritischen Bereiches ausf¨ hrt. Erst wenn dies erf¨ llt ist,
u u
f¨ hrt der Thread den Anweisungsblock aus.
u
Die Threads eines Teams k¨nnen mit einer barrier-
o
Direktive
140 6 OpenMP
#pragma omp barrier
synchronisiert werden, d.h. erst wenn jeder Thread des
Teams diese Direktive erreicht hat, beginnen die Theads
des Teams die Abarbeitung der nachfolgenden Anweisun-
gen.
Durch Angabe der atomic-Direktive k¨nnen bestimm-
o
te Speicherzugriffe als atomare Operationen durchgef¨ hrt
u
werden. Die Syntax dieser Direktive ist
#pragma omp atomic
Zuweisung
Die Zuweisung muss dabei eine der folgenden Formen an-
nehmen:
x binop= E,
x++, ++x, x--, --x,
wobei x einen beliebigen Variablenzugriff, E einen beliebi-
gen skalaren Ausdruck, der x nicht enth¨lt, und binop ∈ {+,
a
-, *, /, &, ^, |, <<, >>} einen bin¨ren Operator bezeichnet.
a
Der Effekt besteht darin, dass nach Auswertung des Aus-
drucks E die angegebene Aktualisierung von x als atomare
Operation erfolgt, d.h. w¨hrend der Aktualisierung kann
a
kein anderer Thread x lesen oder manipulieren. Die Aus-
wertung von E erfolgt nicht als atomare Operation. Prin-
zipiell kann der Effekt einer atomic-Direktive auch durch
eine critical-Direktive erreicht werden, die vereinfachte
Form der atomic-Direktive kann aber evtl. vom Laufzeit-
system f¨ r eine effiziente Implementierung ausgenutzt wer-
u
den. Auch ist es m¨glich mit der atomic-Direktive einzel-
o
ne Feldelemente anzusprechen, wohingegen die critical-
Direktive das gesamte Feld sch¨ tzen w¨ rde. Beispiele f¨ r
u u u
die Verwendung von atomaren Operationen sind:
6.3 Koordination von Threads 141
extern float a[], *p=a, b; int index[];
#pragma omp atomic
a[index[i]] += b;
#pragma omp atomic
p[i] -= 1.0;
Reduktionsoperationen
Um globale Reduktionsoperationen zu erm¨glichen,
o
stellt OpenMP f¨ r die parallel-, sections- und for-
u
Direktiven einen reduction-Parameter mit der Syntax
reduction (op: list)
zur Verf¨ gung. Dabei bezeichnet op ∈{+, -, *, /, &, ^, |,
u
&&, ||} den anzuwendenden Reduktionsoperator, list ist
eine mit Kommata getrennte Liste von Reduktionsvaria-
blen, die im umgebenden Kontext als gemeinsame Variable
deklariert sein m¨ ssen. Der Effekt des Parameters besteht
u
darin, dass bei der Bearbeitung der zugeh¨rigen Direkti-
o
ve f¨ r jede der angegebenen Reduktionsvariablen f¨ r je-
u u
den Thread eine private Kopie der Variablen angelegt wird,
die entsprechend der angegebenen Reduktionsoperation mit
dem neutralen Element dieser Operation initialisiert wird.
Den Reduktionsvariablen k¨nnen w¨hrend der Abarbeitung
o a
des zugeh¨rigen parallelen Bereiches von den verschiedenen
o
Threads Werte zugewiesen werden, die entsprechend der
angegebenen Operation op akkumuliert werden. Am Ende
der Direktive, f¨ r die der reduction-Parameter angegeben
u
wurde, werden die (gemeinsamen) Reduktionsvariablen ak-
tualisiert. Dies geschieht dadurch, dass der urspr¨ ngliche
u
Wert der Reduktionsvariablen und die von den Threads
w¨hrend der Abarbeitung der zugeh¨rigen Direktive errech-
a o
neten Werte der privaten Kopien entsprechend der Reduk-
tionsoperation verkn¨ pft werden. Der so errechnete Wert
u
142 6 OpenMP
wird der Reduktionsvariablen als neuer Wert zugewiesen.
Typischerweise wird der reduction-Parameter zur Akku-
mulation von Werten verwendet. Das folgende Programm-
fragment dient der Akkumulation von Werten in den Ak-
kumulationsvariablen a, y und am:
#pragma omp parallel for reduction (+: a,y)
reduction (||: am)
for (i=0; i<n; i++) {
a += b[i];
y = sum (z, c[i]);
am = am || b[i] == c[i];
}
Die angegebenen Akkumulations-Operationen werden
von den Threads, die den parallelen Bereich ausf¨ hren,
u
f¨ r verschiedene Iterationen der parallelen Schleife durch-
u
gef¨ hrt. Dabei kann eine Reduktionsoperation auch in ei-
u
nem Funktionsaufruf ausgef¨ hrt werden, wie dies f¨ r die
u u
Berechnung von y der Fall ist. Nach Beendigung der par-
allelen Schleife werden die von den verschiedenen Threads
akkumulierten Werte global in den angegebenen Redukti-
onsvariablen akkumuliert.
Sperrmechanismus
F¨ r den Zugriff auf gemeinsame Variablen stellt OpenMP
u
einen Sperrmechanismus zur Verf¨ gung, der uber Lauf-
u ¨
zeitfunktionen verwaltet werden kann. Dabei unterschei-
det OpenMP zwischen einfachen Sperrvariablen vom Typ
omp lock t und schachtelbaren Sperrvariablen vom Typ
omp nest lock t. Der Unterschied besteht darin, dass ei-
ne einfache Sperrvariable nur einmal belegt werden kann,
w¨hrend eine schachtelbare Sperrvariable vom gleichen
a
Thread mehrfach belegt werden kann. Dazu wird f¨ r die
u
6.3 Koordination von Threads 143
schachtelbare Sperrvariable ein Z¨hler gehalten, der die An-
a
zahl der Belegungen mitz¨hlt. Vor Benutzung einer Sperr-
a
variablen muss diese initialisiert werden, wozu die beiden
folgenden Funktionen
void omp init lock (omp lock t *lock)
void omp init nest lock (omp nest lock t *lock)
zur Verf¨ gung stehen. Nach der Initialisierung einer Sperr-
u
variablen ist diese nicht belegt. Zur Zerst¨rung einer initia-
o
lisierten Sperrvariablen stehen die Funktionen
void omp destroy lock (omp lock t *lock)
void omp destroy nest lock (omp nest lock t *lock)
zur Verf¨ gung. Nach Initialisierung einer Sperrvariablen
u
kann diese wie ublich zur Koordination des konkurrieren-
¨
den Zugriffs auf gemeinsame Daten benutzt werden. Zur
Belegung einer Sperrvariablen werden die Funktionen
void omp set lock (omp lock t *lock)
void omp set nest lock (omp nest lock t *lock)
verwendet. Beide Funktionen blockieren den ausf¨ hrenden
u
Thread so lange, bis die angegebene Sperrvariable verf¨ gbar
u
ist. Eine einfache Sperrvariable ist verf¨ gbar, wenn sie
u
von keinem anderen Thread belegt ist. Eine schachtelbare
Sperrvariable ist verf¨ gbar, wenn sie entweder von keinem
u
Thread oder vom ausf¨ hrenden Thread belegt ist. Wenn
u
die angegebene Sperrvariable verf¨ gbar ist, wird sie vom
u
ausf¨ hrenden Thread belegt. F¨ r schachtelbare Sperrvaria-
u u
blen wird der assoziierte Z¨hler inkrementiert.
a
Die Belegung einer Sperrvariablen kann durch Aufruf
der Funktionen
void omp unset lock (omp lock t *lock)
void omp unset nest lock (omp nest lock t *lock)
144 6 OpenMP
wieder freigegeben werden. Dabei kann nur der Thread,
der die Sperrvariable belegt hat, diese auch freige-
ben. Eine normale Sperrvariable wird durch Auf-
ruf von omp unset lock() freigegeben. F¨ r eine u
schachtelbare Sperrvariable dekrementiert der Aufruf
omp unset nest lock() den zugeordneten Z¨hler. Wenn
a
der Z¨hler dadurch den Wert 0 erreicht, wird die Sperrva-
a
riable freigegeben.
Soll beim Versuch der Belegung einer von einem an-
deren Thread belegten Sperrvariablen die Blockierung des
ausf¨ hrenden Threads vermieden werden, k¨nnen die Funk-
u o
tionen
void omp test lock (omp lock t *lock)
void omp test nest lock (omp nest lock t *lock)
verwendet werden. Wenn die angegebene Sperrvariable
verf¨ gbar ist, wird sie wie bei Aufruf von omp set lock()
u
bzw. omp set nest lock() belegt. Wenn die Sperrva-
riable nicht verf¨ gbar ist, wird jedoch der aufrufen-
u
de Thread nicht blockiert. Stattdessen liefern die Funk-
tionsaufrufe den R¨ ckgabewert 0 an den aufrufenden
u
Thread zur¨ ck. Bei erfolgreicher Belegung der Sperrva-
u
riablen liefert omp test lock() einen Wert = 0 zur¨ ck,u
omp test nest lock() liefert den neuen Wert des zugeord-
neten Z¨hlers zur¨ ck.
a u
7
Weitere Ans¨tze
a
Bereits jetzt gibt es eine Reihe von erprobten Programmier-
umgebungen und -bibliotheken zur Programmierung von
Multicore-Prozessoren, die aus der Programmierung mit ge-
meinsamem Adressraum oder dem Multithreading stam-
men. Einige wurden in den letzten Kapiteln vorgestellt.
Der Einsatz popul¨rer Bibliotheken zur Programmierung
a
eines verteilten Speichers wie z.B. MPI ist durch Portie-
rungen ebenfalls bereits m¨glich. F¨ r bestehende parallele
o u
Programme und Programmierer mit Erfahrung in der par-
allelen Programmierung stellt die Nutzung von Multicore-
Prozessoren also einen eher kleinen Schritt in der Pro-
grammiertechnik dar; ein wesentlicher Unterschied liegt in
m¨glicherweise ver¨nderten Effekten der parallelen Lauf-
o a
zeit. F¨ r die weit gr¨ßere Klasse der sequentiellen Pro-
u o
gramme ist der Schritt zur parallelen Programmierung mit
Threads jedoch schwierig und stellt eine große Umstellung
dar [41]. Dies ist auch darin begr¨ ndet, dass die Thread-
u
Programmierung mit Sperrmechnismen und anderen Syn-
chronisationsformen sowie Folgeproblemen wie Deadlocks
einen Programmierstil auf niedriger Ebene darstellt und
146 7 Weitere Ans¨tze
a
auch mit einer Assembler-Programmierung der Parallelver-
arbeitung verglichen wird [63]. Mit solchen Mitteln sind
große Softwareprojekte schwer zu bew¨ltigen.
a
Die Entwicklung zu Multicore-Prozessoren zieht daher
eine Forschungswelle nach sich, die sich mit der nebenl¨ufi-
a
gen und parallelen Programmierung auf h¨herer Ebene
o
besch¨ftigt. Nicht zu vergessen sind dabei Sprachans¨tze,
a a
die durchaus seit einigen Jahren bestehen und durch die
Multicore-Entwicklung an neuer Bedeutung gewinnen. Ei-
nige dieser Sprachen sowie neu entwickelte Sprachen stellen
wir in diesem Kapitel vor. Ein breit diskutierter Program-
mieransatz ist dabei der Transaktionsmechanismus, der die
neueste Entwicklungsrichtung darstellt und mit der wir die
Beschreibung des derzeitigen Standes der Programmierung
von Multicore-Prozessoren abschließen wollen [57, 1].
7.1 Sprachans¨tze
a
¨
Dieser Abschnitt gibt einen kurzen Uberblick uber neuere
¨
Programmiersprachen. Diese Sprachen wurden f¨ r den Be-
u
reich des Hochleistungsrechnens (High Performance Com-
puting) entworfen, k¨nnen aber auch zur Programmierung
o
von Multicore-Systemen eingesetzt werden.
Unified Parallel C
Unified Parallel C (UPC) wurde als Erweiterung von C f¨ r u
den Einsatz auf Parallelrechnern oder Clustersystemen ent-
worfen [21]. UPC basiert auf dem Modell eines partitionier-
ten, globalen Adressraums (partitioned global address space,
PGAS) [16], in dem gemeinsame Variablen abgelegt wer-
den k¨nnen. Jede Variable ist dabei mit einem bestimmten
o
7.1 Sprachans¨tze
a 147
Thread assoziiert, kann aber von jedem anderen Thread ge-
lesen oder manipuliert werden. Die Zugriffszeit auf die Va-
riable ist jedoch f¨ r den assoziierten Thread typischerweise
u
geringer als f¨ r einen anderen Thread. Zus¨tzlich k¨nnen
u a o
f¨ r einen Thread private Daten definiert werden, auf die
u
nur er zugreifen kann.
Parallelit¨t wird in UPC-Programmen dadurch erreicht,
a
dass beim Programmstart eine festgelegte Anzahl von Thre-
ads gestartet wird. Die UPC-Spracherweiterungen von C
beinhalten ein explizit paralleles Ausf¨ hrungsmodell, Spei-
u
cherkonsistenzmodelle f¨ r den Zugriff auf gemeinsame Va-
u
riablen, Synchronisationsoperationen und parallele Schlei-
fen. F¨ r eine detailliertere Beschreibung verweisen wir
u
auf [59, 21]. UPC-Compiler sind f¨ r viele Plattformen
u
verf¨ gbar. Freie UPC-Compiler f¨ r Linux sind z.B. der
u u
Berkeley UPC-Compiler (upc.nersc.gov) oder der GCC
UPC-Compiler (www.intrepid.com/upc). Weitere Spra-
chen, die PGAS realisieren sind Co-Array Fortran Langua-
ge (CAF), eine auf Fortran basierende parallele Sprache,
und Titanium, eine auf Java basierende Sprache ¨hnlich zu
a
UPC.
DARPA HPCS Programmiersprachen
Im Rahmen des DARPA HPCS-Programms (High Produc-
tivity Computing Systems) wurden neue Programmierspra-
chen vorgeschlagen und implementiert, die die Program-
mierung eines gemeinsamen Adressraums mit Sprachkon-
strukten unterst¨ tzen sollen. Zu diesen Sprachen geh¨ren
u o
Fortress, X10 und Chapel.
Fortress wurde von Sun entwickelt und ist eine an
Fortran angelehnte neue objektorientierte Sprache, die die
Programmierung paralleler Systeme durch Verwendung ei-
ner mathematischen Notation erleichtern soll [4]. Fortress
148 7 Weitere Ans¨tze
a
unterst¨ tzt eine parallele Abarbeitung von Programmen
u
durch parallele Schleifen oder die parallele Auswertung von
Funktionsargumenten durch mehrere Threads. Viele Kon-
strukte sind dabei implizit parallel, d.h. die erforderlichen
Threads werden ohne explizite Steuerung im Programm er-
zeugt. So wird z.B. f¨ r jeden Parameter eines Funktions-
u
aufrufs implizit ein separater Thread zur Auswertung ein-
gesetzt, ohne dass dies im Programm angegeben werden
muss. Zus¨tzlich zu diesen impliziten Threads k¨nnen ex-
a o
plizite Threads zur Verarbeitung von Programmteilen ab-
gespalten werden. Die Synchronisation dieser Threads er-
folgt mit atomic-Ausdr¨ cken; diese stellen sicher, dass der
u
Effekt auf den Speicher erst nach kompletter Abarbeitung
des Ausdrucks atomar sichtbar wird, vgl. auch Abschnitt
7.2 uber Transaktionsmechanismen.
¨
X10 wurde von IBM als Erweiterung von Java f¨ r u
¨
den Bereich des Hochleistungsrechnens entwickelt. Ahn-
lich zu UPC basiert X10 auf dem PGAS-Speichermodell
und erweitert dieses zum GALS-Modell (globally asyn-
chronous, locally synchronous) durch Einf¨ hrung von logi-
u
schen Ausf¨ hrungsorten (places genannt) [14]. Threads ei-
u
nes Ausf¨ hrungsortes haben eine lokal synchrone Sicht auf
u
einen gemeinsamen Adressraum, Threads unterschiedlicher
Ausf¨ hrungsorte werden dagegen asynchron zueinander
u
ausgef¨ hrt. X10 beinhaltet eine Vielzahl von Operationen
u
zur Manipulation von Feldvariablen und Teilen von Feldva-
riablen. Mithilfe von Feldverteilungen kann die Aufteilung
von Feldern auf unterschiedliche Ausf¨ hrungsorte im glo-
u
balen Speicher spezifiziert werden. F¨ r die Synchronisati-
u
on von Threads stehen atomic-Bl¨cke zur Verf¨ gung, die
o u
eine atomare Ausf¨ hrung von Anweisungen bewirken. Die
u
korrekte Verwendung von Sperrmechanismen, z.B. durch
synchronized-Bl¨cke oder -Methoden, wird dadurch dem
o
Laufzeitsystem ubertragen.
¨
7.1 Sprachans¨tze
a 149
Chapel wurde von Cray Inc. als neue parallele Pro-
grammiersprache f¨ r Hochleistungsrechnen entworfen [18].
u
Die verwendeten Konstrukte sind teilweise an High-
Performance Fortran (HPF) angelehnt. Chapel basiert wie
Fortress und X10 auf dem Modell eines globalen Adress-
raums, in dem Datenstrukturen wie z.B. Felder abgelegt
und zugegriffen werden k¨nnen. Die unterst¨tzte Paralle-
o u
lit¨t ist threadbasiert: bei Programmstart gibt es einen
a
Haupt-Thread; durch Verwendung spezieller Sprachkon-
strukte (parallele Schleifen) k¨nnen weitere Threads er-
o
zeugt werden, die dann vom Laufzeitsystem verwaltet wer-
den. Ein explizites Starten und Beenden von Threads durch
den Programmierer entf¨llt damit. F¨ r die Koordination
a u
von Berechnungen auf gemeinsamen Daten stehen Synchro-
nisationsvariablen und atomic-Bl¨cke zur Verf¨ gung.
o u
Global Arrays
Zur Unterst¨ tzung der Programmierung von Anwendungen
u
des wissenschaftlichen Rechnens, die feldbasierte Daten-
strukturen wie z.B. Matrizen verwenden, wurde der GA-
Ansatz (Global Arrays) entwickelt [51]. Dieser wird als Bi-
bliothek mit Sprachanbindung f¨ r C, C++ und Fortran f¨ r
u u
unterschiedliche Plattformen zur Verf¨ gung gestellt. Der
u
GA-Ansatz basiert auf einem gemeinsamen Adressraum, in
dem Felddatenstrukturen (globale Felder) so abgelegt wer-
den k¨nnen, dass jedem Prozess ein logischer Block des glo-
o
balen Feldes zugeteilt ist; auf diesen Block kann der Pro-
zess schneller zugreifen als auf die anderen Bl¨cke. Die GA-
o
Bibliothek stellt Basisoperationen f¨ r den gemeinsamen
u
Adressraum (put, get, scatter, gather) sowie atomare Ope-
rationen und Sperrmechanismen f¨ r den Zugriff auf globale
u
Felder zur Verf¨ gung. Der Datenaustausch zwischen Pro-
u
zessoren kann uber die globalen Felder, aber auch uber eine
¨ ¨
150 7 Weitere Ans¨tze
a
Message-Passing-Bibliothek wie MPI erfolgen. Ein wichti-
ges Anwendungsgebiet des GA-Ansatzes liegt im Bereich
chemischer Simulationen.
7.2 Transaktionsspeicher
F¨ r die Synchronisation von Threads beim Zugriff auf ge-
u
meinsame Daten werden in den meisten Ans¨tzen Sperr-
a
variablen (Mutexvariablen) und kritische Bereiche
verwendet. Dabei wird typischerweise wie folgt vorgegan-
gen:
• der Programmierer identifiziert kritische Bereiche im
Programm und sch¨ tzt diese explizit mit Sperrvariablen
u
(lock/unlock-Mechanismus);
• der Sperrvariablen-Mechanismus sorgt daf¨ r, dass ein
u
kritischer Bereich jeweils nur von einem Thread aus-
gef¨ hrt wird.
u
Der Ansatz mit Sperrvariablen kann zu einer Sequentia-
lisierung der Abarbeitung von kritischen Bereichen f¨ hren
u
was je nach Anwendung die Skalierbarkeit erheblich beein-
tr¨chtigt, da die kritischen Bereiche zum Flaschenhals wer-
a
den k¨nnen. Dies gilt insbesondere dann, wenn viele Thre-
o
ads verwendet werden und die kritischen Bereiche eine gro-
be Granularit¨t haben, also relativ lang sind.
a
F¨ r heutige Multicore-Prozessoren spielt dieses Pro-
u
blem noch eine untergeordnete Rolle, da nur wenige Pro-
zessorkerne verwendet werden. F¨ r zuk¨ nftige Multicore-
u u
Prozessoren mit Dutzenden von Prozessorkernen oder
beim Zusammenschalten mehrere Multicore-Prozessoren zu
Clustersystemen muss das Problem sehr wohl beachtet wer-
den. Als alternativer Ansatz zum Sperrmechanismus wur-
de daher die Verwendung eines sogenannten Transakti-
onsspeichers (transactional memory) vorgeschlagen, siehe
7.2 Transaktionsspeicher 151
z.B. [1, 7, 29]. Eine Transaktion wird dabei als eine end-
liche Folge von Instruktionen definiert, die von einem ein-
zelnen Thread ausgef¨ hrt wird und bei deren Ausf¨ hrung
u u
folgende Eigenschaften gelten:
• Serialisierbarkeit: Die Transaktionen eines Pro-
gramms erscheinen f¨ r alle beteiligten Threads sequen-
u
tiell angeordnet; kein Thread beobachtet eine Ver-
schr¨nkung von Instruktionen verschiedener Transak-
a
tionen; f¨ r jeden Thread erscheinen die Transaktionen
u
in der gleichen Reihenfolge.
• Atomarit¨t: Die von den Instruktionen einer Trans-
a
¨
aktion durchgef¨ hrten Anderungen des gemeinsa-
u
men Speichers werden f¨ r die die Transaktion nicht
u
ausf¨ hrenden Threads erst am Ende der Transaktion
u
atomar sichtbar (commit); eine abgebrochene Trans-
aktion hat keinen Effekt auf den gemeinsamen Speicher
(abort).
Die mit einem Sperrmechanismus definierten kritischen Be-
reiche sind in diesem Sinne nicht atomar, da der Effekt auf
den gemeinsamen Speicher direkt sichtbar wird. Die Ver-
wendung des Transaktionsmechanismus ist also nicht nur
eine Programmiertechnik, sondern kann auch andere Er-
gebnisse als ein Sperrmechanismus bewirken.
Die Verwendung von Transaktionen erfordert die
Einf¨ hrung neuer Konstrukte, etwa auf Sprachebene. Daf¨ r
u u
wurde die Einf¨ hrung von atomic-Bl¨cken zur Identifikati-
u o
on von Transaktionen vorgeschlagen [1]: anstatt der Ver-
wendung einer Sperrvariablen wird ein Sprachkonstrukt
atomic{B} vorgeschlagen, das die Anweisungen in Block
B als Transaktion ausf¨hrt.
u
Die im Rahmen des HPCS-Projektes entwickelten Spra-
chen - Fortress von Sun [4], X10 von IBM [14] und Cha-
152 7 Weitere Ans¨tze
a
pel von Cray [18] - enthalten solche Konstrukte zur Un-
terst¨ tzung von Transaktionen.
u
Der Unterschied zwischen der Verwendung von Sperrva-
riablen und atomaren Bl¨cken ist in Abb. 7.1 am Beispiel
o
eines threadsicheren Kontozugriffs veranschaulicht. Ein
sperrorientierter Zugriff wird durch die Klasse LockAccount
mit Hilfe eines Java synchronized-Blocks realisiert. Ein Auf-
ruf von add() leitet den Aufruf einfach an die gleichnamige
Methode der nicht-threadsicheren Account-Klasse weiter,
die wir hier als gegeben voraussetzen. Die Ausf¨ hrung des
u
synchronized-Blocks bewirkt die Aktivierung eines Sperr-
mechanismus bzgl. des Objekts mutex; dieser stellt eine
Sequentialisierung des Zugriffs sicher. Ein transaktionsori-
entierter Zugriff k¨nnte durch die Klasse AtomicAccount
o
realisiert werden, die einen atomic-Befehl verwendet, um
die Aktivierung der nicht-threadsicheren add()-Methode
der Account-Klasse als Transaktion zu identifizieren. Da-
mit w¨re das Laufzeitsystem f¨ r die Sicherstellung der Se-
a u
rialisierbarkeit und Atomarit¨t verantwortlich, m¨ sste aber
a u
nicht unbedingt eine Sequentialisierung erzwingen, wenn
dies nicht notwendig ist. Dabei ist zu beachten, dass atomic-
Bl¨cke (noch) nicht Teil der Java-Sprache sind.
o
Der Vorteil der Verwendung von Transaktionen liegt
darin, dass das Laufzeitsystem auch mehrere Transaktio-
nen parallel zueinander ausf¨ hren kann, wenn das Spei-
u
cherzugriffsmuster der Transaktionen dies zuließe. Bei Ver-
wendung einfacher Sperrvariablen ist dies nicht ohne weite-
res m¨glich. Sperrvariablen k¨nnen zwar verwendet werden,
o o
um komplexere Synchronisationsmechanismen zu definie-
ren, die den gleichzeitigen Zugriff mehrerer Threads erlau-
ben, dies erfordert aber einen erheblichen zus¨tzlichen Pro-
a
grammieraufwand. Ein Beispiel sind Lese-Schreibsperren,
die mehrere Lesezugriffe gleichzeitig, aber jeweils nur einen
Schreibzugriff erlauben, siehe Abschnitt 4.4. F¨ r die Ver-
u
7.2 Transaktionsspeicher 153
class LockAccount implements Account {
Object mutex;
Account a;
LockAccount (Account a) {
this.a = a;
mutex = New Object();
}
public int add (int x) {
synchronized (mutex) {
return a.add(x);
}
}
...
}
class AtomicAccount implements Account {
Account a;
AtomicAccount (Account a) {
this.a = a;
}
public int add (int x) {
atomic {
return a.add(x);
}
}
...
}
Abbildung 7.1. Vergleich zwischen sperrorientierter und trans-
aktionsorientierter (Vorschlag) Realisierung eines Kontozugriffs.
154 7 Weitere Ans¨tze
a
wendung von Transaktionen wird damit eine bessere Ska-
lierbarkeit erwartet als bei der Verwendung von Sperrvaria-
blen.
Die Verarbeitung von Transaktionen stellt bestimmte
Anforderungen an das Laufzeitsystem:
• Versionskontrolle: Der Effekt einer Transaktion darf
erst am Ende der Transaktion sichtbar werden. Da-
mit muss das Laufzeitsystem w¨hrend der Abarbeitung
a
einer Transaktion auf einem separaten Datensatz ar-
beiten. Wird die Transaktion abgebrochen, bleibt der
alte Datensatz erhalten. Bei erfolgreicher Ausf¨ hrung
u
der Transaktion wird der neue Datensatz am Ende der
Transaktion global sichtbar.
• Erkennen von Konflikten: Sollen mehrere Transak-
tionen zur Verbesserung der Skalierbarkeitseigenschaf-
ten konkurrierend ausgef¨ hrt werden, muss sicherge-
u
stellt sein, dass sie nicht gleichzeitig auf dieselben Da-
ten zugreifen. Dazu ist eine Analyse der Speicherzu-
griffsmuster der Transaktionen durch das Laufzeitsys-
tem notwendig.
Die Verarbeitung von Transaktionen ist zur Zeit ein aktives
Forschungsgebiet und so wird sicherlich eine geraume Zeit
vergehen, bis der Ansatz in Standard-Programmiersprachen
verwendet werden kann. Der Ansatz wird jedoch als vielver-
sprechend eingesch¨tzt, da er einen abstrakteren Mechanis-
a
mus als Sperrvariablen zur Verf¨ gung stellt, der Probleme
u
wie Deadlocks vermeidet, zu einer guten Skalierbarkeit von
threadbasierten Anwendungsprogrammen f¨ hren kann und
u
durch den Programmierer leichter anwendbar ist.
Literatur
1. A. Adl-Tabatabai, C. Kozyrakis, and B. Saha. Unlocking
concurrency. ACM Queue, 4(10):24–33, Dec 2006.
2. A. Aho, M. Lam, R. Sethi, and J. Ullman. Compilers: Prin-
ciples, Techniques & Tools. Pearson-Addison Wesley, 2007.
3. S. Akhter and J. Roberts. Multi-Core Programming – Incre-
asing Performance through Software Multi-threading. Intel
Press, 2006.
4. Eric Allen, David Chase, Joe Hallett, Victor Luchangco,
Jan-Willem Maessen, Sukyoung Ryu, Guy L. Steele, Jr., and
Sam Tobin-Hochstadt. The Fortress Language Specification,
version 1.0beta, March 2007.
5. R. Allen and K. Kennedy. Optimizing Compilers for Modern
Architectures. Morgan Kaufmann, 2002.
6. G. Amdahl. Validity of the Single Processor Approach to
Achieving Large-Scale Computer Capabilities. In AFIPS
Conference Proceedings, volume 30, pages 483–485, 1967.
7. K. Asanovic, R. Bodik, B.C. Catanzaro, J.J. Gebis, P. Hus-
bands, K. Keutzer, D.A. Patterson, W.L. Plishker, J. Shalf,
S.W. Williams, and K.A. Yelick. The Landscape of Parallel
Computing Research: A View from Berkeley. Technical Re-
port UCB/EECS-2006-183, EECS Department, University
of California, Berkeley, December 2006.
156 Literatur
8. R. Bird. Introduction to Functional Programming using Has-
kell. Prentice Hall, 1998.
9. A. Birrell. An introduction to programming with threads.
Technical Report Research Report 35, Compaq Systems Re-
search center, Palo Alto, 1989.
10. A. Bode and W. Karl. Multicore: Architektur. Springer
Verlag, 2007.
11. D. R. Butenhof. Programming with POSIX Threads.
Addison-Wesley, 1997.
12. N. Carriero and D. Gelernter. Linda in Context. Com-
mun. ACM, 32(4):444–458, 1989.
13. R. Chandra, L. Dagum, D. Koher, D. Maydan, J. McDonald,
and R. Menon. Parallel Programming in OpenMP. Morgan
Kaufmann, 2001.
14. P. Charles, C. Grothoff, V.A. Saraswat, C. Donawa, A. Kiel-
stra, K. Ebcioglu, C. von Praun, and V. Sarkar. X10:
an object-oriented approach to non-uniform cluster compu-
ting. In R. Johnson and R.P. Gabriel, editors, Proceedings
of the 20th Annual ACM SIGPLAN Conference on Object-
Oriented Programming, Systems, Languages, and Applicati-
ons (OOPSLA), pages 519–538. ACM, October 2005.
15. M.E. Conway. A Multiprocessor System Design. In Proc.
AFIPS 1963 Fall Joint Computer Conference, volume 24,
pages 139–146. NewYork: Spartan Books, 1963.
16. D.E. Culler, A.C. Arpaci-Dusseau, S.C. Goldstein, A. Kris-
hnamurthy, S. Lumetta, T. van Eicken, and K.A. Yelick.
Parallel programming in Split-C. In Proceedings of Super-
computing, pages 262–273, 1993.
17. D.E. Culler, J.P. Singh, and A. Gupta. Parallel Compu-
ter Architecture: A Hardware Software Approach. Morgan
Kaufmann, 1999.
18. D. Callahan and B. L. Chamberlain and H. P. Zima. The
Cascade High Productivity Language. In IPDPS, pages 52–
60. IEEE Computer Society, 2004.
19. E.W. Dijkstra. Cooperating Sequential Processes. In F. Ge-
nuys, editor, Programming Languages, pages 43–112. Aca-
demic Press, 1968.
Literatur 157
20. J. Doweck. Intel Smart Memory Access: Minimizing
Latency on Intel Core Microarchitecture. Technolo-
gy@IntelMagazine, September 2006.
21. T. El-Ghazawi, W. Carlson, T. Sterling, and K. Yelick.
UPC: Distributed Sahred Memory Programming. Wiley,
2005.
22. D. Flanagan. Java in a Nutshell. O’Reilly, 2005.
23. M.J. Flynn. Some Computer Organizations and their Effec-
tiveness. IEEE Transactions on Computers, 21(9):948–960,
1972.
24. S. Gochman, A. Mendelson, A. Naveh, and E. Rotem. In-
troduction to Intel Core Duo Processor Architecture. Intel
Technology Journal, 10(2):89–97, May 2006.
25. B. Goetz. Java Concurrency in Practice. Addison Wesley,
2006.
26. W. Gropp, E. Lusk, and A. Skjellum. MPI – Eine
Einf¨hrung. Oldenbourg Verlag, 2007.
u
27. J. Held and J. Bautista ans S. Koehl. From a Few Cores to
Many – A Tera-Scale Computing Research Overview. Intel
White Paper, Intel, 2006.
28. J. L. Hennessy and D. A. Patterson. Computer Architecture
— A Quantitative Approach. Morgan Kaufmann, 2007.
29. M. Herlihy and J.E.B. Moss. Transactional Memory: Ar-
chitectural Support for Lock-free Data Stractures. In Proc.
of the 20th Ann. Int. Symp. on Computer Architecture (IS-
CA’93), pages 289–300, 1993.
30. J. Hippold and G. R¨ nger. Task Pool Teams: A Hybrid Pro-
u
gramming Environment for Irregular Algorithms on SMP
Clusters. Concurrency and Computation: Practice and Ex-
perience, 18(12):1575–1594, 2006.
31. C.A.R. Hoare. Monitors: An Operating Systems Structuring
Concept. Commun. ACM, 17(10):549–557, 1974.
32. R.W. Hockney. The Science of Computer Benchmarking.
SIAM, 1996.
33. P. Hudak and J. Fasel. A Gentle Introduction to Haskell.
ACM SIGPLAN Notices, 27, No.5, May 1992.
158 Literatur
34. J.A. Kahle, M.N. Day, H.P. Hofstee, C.R. Johns, T.R. Maeu-
rer, and D. Shippy. Introduction to the Cell Multiproces-
sor. IBM Journal of Research and Development, September
2005.
35. St. Kleiman, D. Shah, and B. Smaalders. Programming with
Threads. Prentice Hall, 1996.
36. G. Koch. Discovering Multi-Core:Extending the Benefits of
Moore’s Law. Intel White Paper, Technology@Intel Maga-
zine, 2005.
37. P.M. Kogge. An Exploitation of the Technology Space for
Multi-Core Memory/Logic Chips for Highly Scalable Paral-
lel Systems. In Proceedings of the Innovative Architecture for
Future Generation High-Performance Processors and Sys-
tems. IEEE, 2005.
38. M. Korch and T. Rauber. A comparison of task pools for
dynamic load balancing of irregular algorithms. Concur-
rency and Computation: Practice and Experience, 16:1–47,
January 2004.
39. K. Krewell. Cell moves into the Limelight. Micropro-
cessor Report, Reed Business Information, February 2005.
www.MPRonline.com.
40. D. Lea. Concurrent Programming in Java: Design Princip-
les and Patterns. Addison Wesley, 1999.
41. E.A. Lee. The Problem with Threads. IEEE Computer,
39(5):33–42, 2006.
42. B. Lewis and D. J. Berg. Multithreaded Programming with
Pthreads. Prentice Hall, 1998.
43. D.T. Marr, F. Binns, D. L. Hill, G. Hinton, D.A. Koufaty,
J.A. Miller, and M. Upton. Hyper-threading technology ar-
chitecture and microarchitecture. Intel Technology Journal,
6(1):4–15, February 2002.
44. D.T. Marr, F. Binus, D.L. Hill, G. Hinton, D.A. Konfaty,
J.A. Miller, and M. Upton. Hyper-Threading Technology
Architecture and Microarchitecture. Intel Technology Jour-
nal, 6(1):4–15, 2002.
45. T. Mattson, B. Sandor, and B. Massingill. Pattern for Par-
allel Programming. Pearson – Addison Wesley, 2005.
Literatur 159
46. M.K. McKusick, K. Bostic, M.J. Karels, and J.S. Quarter-
man. The Design and Implementation of the 4.4 BSD Ope-
rating System. Addison-Wesley, 1996.
47. A. Mendelson, J. Mandelblat, S. Gochman, A. Shemer,
R. Chabukswar, E. Niemeyer, and A. Kumar. CMP Im-
plementation in Systems Based on the Intel Core Duo Pro-
cessor. Intel Technology Journal, 10(2):99–107, May 2006.
48. A. Naveh, E. Rotem, A. Mendelson, S. Gochman, R. Cha-
bukswar, K. Krishnan, and A. Kumar. Power and Thermal
Management in the Intel Core Duo Processor. Intel Tech-
nology Journal, 10(2):109–122, May 2006.
49. B. Nichols, D. Buttlar, and J. Proulx Farrell. Pthreads Pro-
gramming. O’Reilly & Associates, 1997.
50. M.A. Nichols, H.J. Siegel, and H.G. Dietz. Data Manage-
ment and Control–Flow Aspects of an SIMD/SPMD Paral-
lel Language/Compiler. IEEE Transactions on Parallel and
Distributed Systems, 4(2):222–234, 1993.
51. J. Nieplocha, J. Ju, M.K. Krishnan, B. Palmer, and V. Tip-
paraju. The Global Arrays User’s Manual. Technical Report
PNNL-13130, Pacific Northwest National Laboratory, 2002.
52. S. Oaks and H. Wong. Java Threads. 3. Auflage, O’Reilly,
2004.
53. OpenMP Application Program Interface, Version 2.5, May
2005.
54. D.A. Patterson and J.L. Hennessy. Computer Organizati-
on & Design — The Hardware/Software Interface. Morgan
Kaufmann, 2006.
55. C.D. Polychronopoulos. Parallel Programming and Compi-
lers. Kluwer Academic Publishers, 1988.
56. S. Prasad. Multithreading Programming Techniques.
McGraw-Hill, 1997.
57. R. Rajwar and J. Goodman. Transactional Execution: To-
wards Reliable, High-Performance Multithreading. IEEE
Micro, pages 117–125, 2003.
58. R.M. Ramanathan. Intel Multi-core Processors: Leading the
Next Digital Revaluation. Intel White Paper, Technology-
Intel Magazine, 2005.
160 Literatur
59. T. Rauber and G. R¨ nger. Parallele Programmierung, 2te
u
Auflage. eXamens.press. Springer, 2007.
60. D. Skillicorn and D. Talia. Models and Languages for Paral-
lel Computation. ACM Computing Surveys, 30(2):123–169,
1998.
61. M. Snir, S. Otto, S. Huss-Ledermann, D. Walker,
and J. Dongarra. MPI: The Complete Reference.
MIT Press, Camdridge, MA, 1996. Zugreifbar uber:
¨
www.netlib.org/utk/papers/mpi book/mpi book.html.
62. H. Sutter. The free lunch is over – a fundamental turn
toward concurrency in software. Dr.Dobb’s Jouernal, 30(3),
2005.
63. H. Sutter and J. Larus. Software and the Concurrency Re-
volution. 2005, 3(7):54–62, ACM Queue.
64. S. Thompson. Haskell – The Craft of Functional Program-
ming. Addison Wesley, 1999.
65. L.G. Valiant. A Bridging Model for parallel Computation.
Commun. ACM, 33(8):103–111, 1990.
66. M. Wolfe. High Performance Compilers for Parallel Com-
puting. Addison-Wesley, 1996.
67. S.N. Zheltov and S.V. Bratanov. Measuring HT-Enabled
Multi-Core: Advantages of a Thread-Oriented Approach.
Technology & Intel Magazine, December 2005.
0 comments
Post a comment