multicore parallele programmierung kng617

172
Informatik im Fokus Herausgeber: Prof. Dr. O. Günther Prof. Dr. W. Karl Prof. Dr. R. Lienhart Prof. Dr. K. Zeppenfeld

Upload: guest465f28

Post on 20-Jan-2015

1.557 views

Category:

Documents


5 download

DESCRIPTION

 

TRANSCRIPT

Page 1: Multicore Parallele Programmierung Kng617

Informatik im Fokus

Herausgeber:

Prof. Dr. O. GüntherProf. Dr. W. KarlProf. Dr. R. LienhartProf. Dr. K. Zeppenfeld

Page 2: Multicore Parallele Programmierung Kng617

Informatik im Fokus

Rauber, T.; Rünger, G.Multicore: ParalleleProgrammierung. 2008

El Moussaoui, H.; Zeppenfeld, K.AJAX. 2008

Behrendt, J.; Zeppenfeld, K.Web 2.0. 2008

Bode, A.; Karl, W.Multicore-Architekturen. 2008

Page 3: Multicore Parallele Programmierung Kng617

Thomas Rauber · Gudula Rünger

Multicore:Parallele Programmierung

123

Page 4: Multicore Parallele Programmierung Kng617

Prof. Dr. Thomas Rauber

Universität BayreuthLS Angewandte Informatik IIUniversitätsstr. 3095447 [email protected]

Prof. Dr. Gudula Rünger

TU ChemnitzFakultät für InformatikStraße der Nationen 6209107 [email protected]

Herausgeber:

Prof. Dr. O. GüntherHumboldt Universität zu Berlin

Prof. Dr. W. KarlUniversität Karlsruhe (TH)

Prof. Dr. R. LienhartUniversität Augsburg

Prof. Dr. K. ZeppenfeldFachhochschule Dortmund

ISBN 978-3-540-73113-9 e-ISBN 978-3-540-73114-6

DOI 10.1007/978-3-540-73114-6

ISSN 1865-4452

Bibliografische Information der Deutschen NationalbibliothekDie Deutsche Nationalbibliothek verzeichnet diese Publikation in der DeutschenNationalbibliografie; detaillierte bibliografische Daten sind im Internet überhttp://dnb.d-nb.de abrufbar.

© 2008 Springer-Verlag Berlin Heidelberg

Dieses Werk ist urheberrechtlich geschützt. Die dadurch begründeten Rechte, insbesondere die derÜbersetzung, des Nachdrucks, des Vortrags, der Entnahme von Abbildungen und Tabellen, der Funk-sendung, der Mikroverfilmung oder der Vervielfältigung auf anderen Wegen und der Speicherung inDatenverarbeitungsanlagen, bleiben, auch bei nur auszugsweiser Verwertung, vorbehalten. Eine Ver-vielfältigung dieses Werkes oder von Teilen dieses Werkes ist auch im Einzelfall nur in den Grenzender gesetzlichen Bestimmungen des Urheberrechtsgesetzes der Bundesrepublik Deutschland vom 9.September 1965 in der jeweils geltenden Fassung zulässig. Sie ist grundsätzlich vergütungspflichtig.Zuwiderhandlungen unterliegen den Strafbestimmungen des Urheberrechtsgesetzes.

Die Wiedergabe von Gebrauchsnamen, Handelsnamen, Warenbezeichnungen usw. in diesem Werkberechtigt auch ohne besondere Kennzeichnung nicht zu der Annahme, dass solche Namen im Sinneder Warenzeichen- und Markenschutz-Gesetzgebung als frei zu betrachten wären und daher vonjedermann benutzt werden dürften. Text und Abbildungen wurden mit größter Sorgfalt erarbeitet.Verlag und Autor können jedoch für eventuell verbliebene fehlerhafte Angaben und deren Folgenweder eine juristische Verantwortung noch irgendeine Haftung übernehmen.

Einbandgestaltung: KünkelLopka Werbeagentur, Heidelberg

Gedruckt auf säurefreiem Papier

9 8 7 6 5 4 3 2 1

springer.com

Page 5: Multicore Parallele Programmierung Kng617

Vorwort

Nach vielen Jahren stetigen technologischen Fortschritts inder Mikroprozessorentwicklung stellt die Multicore-Techno-logie die neueste Entwickungsstufe dar. Fuhrende Hard-warehersteller wie Intel, AMD, Sun oder IBM liefern seit2005 Mikroprozessoren mit mehreren unabhangigen Pro-zessorkernen auf einem einzelnen Prozessorchip. Im Jahr2007 verwendet ein typischer Desktop-PC je nach Ausstat-tung einen Dualcore- oder Quadcore-Prozessor mit zweibzw. vier Prozessorkernen. Die Ankundigungen der Prozes-sorhersteller zeigen, dass dies erst der Anfang einer langerandauernden Entwicklung ist. Eine Studie von Intel prog-nostiziert, dass im Jahr 2015 ein typischer Prozessorchipaus Dutzenden bis Hunderten von Prozessorkernen be-steht, die zum Teil spezialisierte Aufgaben wie Verschlusse-lung, Grafikdarstellung oder Netzwerkmanagement wahr-nehmen. Ein Großteil der Prozessorkerne steht aber furAnwendungsprogramme zur Verfugung und kann z.B. furBuro- oder Unterhaltungssoftware genutzt werden.

Die von der Hardwareindustrie vorgegebene Entwick-lung hin zu Multicore-Prozessoren bietet fur die Software-

Page 6: Multicore Parallele Programmierung Kng617

VI Vorwort

entwickler also neue Moglichkeiten, die in der Bereitstel-lung zusatzlicher Funktionalitaten der angebotenen Soft-ware liegen, die parallel zu den bisherigen Funktionalitatenausgefuhrt werden konnen, ohne dass dies beim Nutzer zuWartezeiten fuhrt. Diese Entwicklung stellt aber auch einenParadigmenwechsel in der Softwareentwicklung dar, wegvon der herkommlichen sequentiellen Programmierung hinzur 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 Einfuhrung von Multicore-Prozessorenin alle Bereiche der Softwareentwicklung getragen werdenund so fur viele Softwareentwickler eine neue Herausforde-rung entsteht.

Das Ziel dieses Buches ist es, dem Leser einen ers-ten Einblick in die fur Multicore-Prozessoren geeignetenparallelen Programmiertechniken und -systeme zu geben.Programmierumgebungen wie Pthreads, Java-Threads undOpenMP werden vorgestellt. Die Darstellung geht dabeidavon aus, dass der Leser mit Standardtechniken der Pro-grammierung vertraut ist. Das Buch enthalt zahlreiche Hin-weise auf weiterfuhrende Literatur sowie neuere Entwick-lungen wie etwa neue Programmiersprachen. Fur Hilfe beider Erstellung des Buches und Korrekturen danken wir JorgDummler, Monika Glaser, Marco Hobbel, Raphael Kunisund Michael Schwind. Dem Springer-Verlag danken wir furdie gute Zusammenarbeit.

Bayreuth, Chemnitz, Thomas RauberAugust 2007 Gudula Runger

Page 7: Multicore Parallele Programmierung Kng617

Inhaltsverzeichnis

1 Kurzuberblick Multicore-Prozessoren . . . . . . 11.1 Entwicklung der Mikroprozessoren . . . . . . . . . 11.2 Parallelitat auf Prozessorebene . . . . . . . . . . . . 41.3 Architektur von Multicore-Prozessoren . . . . . 81.4 Beispiele . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13

2 Konzepte paralleler Programmierung . . . . . . 212.1 Entwurf paralleler Programme . . . . . . . . . . . . 222.2 Klassifizierung paralleler Architekturen . . . . . 272.3 Parallele Programmiermodelle . . . . . . . . . . . . . 292.4 Parallele Leistungsmaße . . . . . . . . . . . . . . . . . . 35

3 Thread-Programmierung . . . . . . . . . . . . . . . . . . 393.1 Threads und Prozesse . . . . . . . . . . . . . . . . . . . . 393.2 Synchronisations-Mechanismen . . . . . . . . . . . . 463.3 Effiziente und korrekte Thread-Programme . 513.4 Parallele Programmiermuster . . . . . . . . . . . . . 543.5 Parallele Programmierumgebungen . . . . . . . . 61

Page 8: Multicore Parallele Programmierung Kng617

VIII Inhaltsverzeichnis

4 Programmierung mit Pthreads . . . . . . . . . . . . 634.1 Threaderzeugung und -verwaltung . . . . . . . . . 634.2 Koordination von Threads . . . . . . . . . . . . . . . . 664.3 Bedingungsvariablen . . . . . . . . . . . . . . . . . . . . . 704.4 Erweiterter Sperrmechanismus . . . . . . . . . . . . 754.5 Implementierung eines Taskpools . . . . . . . . . . 78

5 Java-Threads . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 855.1 Erzeugung von Threads in Java . . . . . . . . . . . 855.2 Synchronisation von Java-Threads . . . . . . . . . 915.3 Signalmechanismus in Java . . . . . . . . . . . . . . . 1015.4 Erweiterte Synchronisationsmuster . . . . . . . . . 1095.5 Thread-Scheduling in Java . . . . . . . . . . . . . . . . 1135.6 Paket java.util.concurrent . . . . . . . . . . . . 115

6 OpenMP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1256.1 Programmiermodell . . . . . . . . . . . . . . . . . . . . . . 1256.2 Spezifikation der Parallelitat . . . . . . . . . . . . . . 1276.3 Koordination von Threads . . . . . . . . . . . . . . . . 139

7 Weitere Ansatze . . . . . . . . . . . . . . . . . . . . . . . . . . . 1457.1 Sprachansatze . . . . . . . . . . . . . . . . . . . . . . . . . . . 1467.2 Transaktionsspeicher . . . . . . . . . . . . . . . . . . . . . 150

Literatur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155

Index . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 161

Page 9: Multicore Parallele Programmierung Kng617

1

KurzuberblickMulticore-Prozessoren

Die Entwicklung der Mikroprozessoren hat in den letztenJahrzehnten durch verschiedene technologische Innovatio-nen immer leistungsstarkere Prozessoren hervorgebracht.Multicore-Prozessoren stellen einen weiteren Meilenstein inder Entwicklung dar.

1.1 Entwicklung der Mikroprozessoren

Prozessorchips sind intern aus Transistoren aufgebaut, de-ren Anzahl ein ungefahres Maß fur die Komplexitat undLeistungsfahigkeit des Prozessors ist. Das auf empirischenBeobachtungen beruhende Gesetz von Moore besagt,dass die Anzahl der Transistoren eines Prozessorchips sichalle 18 bis 24 Monate verdoppelt. Diese Beobachtung wurde1965 von Gordon Moore zum ersten Mal gemacht und giltnun seit uber 40 Jahren. Ein typischer Prozessorchip ausdem Jahr 2007 besteht aus ca. 200-400 Millionen Transis-toren: beispielsweise besteht ein Intel Core 2 Duo Prozessor

Page 10: Multicore Parallele Programmierung Kng617

2 1 Kurzuberblick Multicore-Prozessoren

aus ca. 291 Millionen Transistoren, ein IBM Cell-Prozessoraus ca. 250 Millionen Transistoren.

Die Erhohung der Transistoranzahl ging in der Ver-gangenheit mit einer Erhohung der Taktrate einher. Diessteigerte die Verarbeitungsgeschwindigkeit der Prozesso-ren und die Taktrate eines Prozessors wurde oftmals alsalleiniges Merkmal fur dessen Leistungsfahigkeit wahrge-nommen. Gemeinsam fuhrte die Steigerung der Taktrateund der Transistoranzahl zu einer durchschnittlichen jahr-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 fureine Beschreibung der oft verwendeten SPEC-Benchmarks.Eine Erhohung der Taktrate im bisherigen Umfang ist je-doch fur die Zukunft nicht zu erwarten. Dies ist darin be-grundet, dass mit einer Erhohung der Taktrate auch dieLeistungsaufnahme, also der Energieverbrauch des Prozes-sors, ansteigt, wobei ein Großteil des verbrauchten Stromsin Warme umgewandelt wird und uber Lufter abgefuhrtwerden muss. Das Gesetz von Moore scheint aber bis aufweiteres seine Gultigkeit zu behalten.

Die steigende Anzahl verfugbarer Transistoren wurdein der Vergangenheit fur eine Vielzahl weiterer architekto-nischer Verbesserungen genutzt, die die Leistungsfahigkeitder Prozessoren erheblich gesteigert hat. Dazu gehoren u.a.

• die Erweiterung der internen Wortbreite auf 64 Bits,• die Verwendung interner Pipelineverarbeitung fur die

ressourcenoptimierte Ausfuhrung aufeinanderfolgenderMaschinenbefehle,

• die Verwendung mehrerer Funktionseinheiten, mit de-nen voneinander unabhangige Maschinenbefehle paral-lel zueinander abgearbeitet werden konnen und

Page 11: Multicore Parallele Programmierung Kng617

1.1 Entwicklung der Mikroprozessoren 3

• die Vergroßerung der prozessorlokalen Cachespeicher.

Wesentliche Aspekte der Leistungssteigerung sind alsodie Erhohung der Taktrate und der interne Einsatz paralle-ler Abarbeitung von Instruktionen, z.B. durch das Duplizie-ren von Funktionseinheiten. Die Grenzen beider Entwick-lungen sind jedoch abzusehen: Ein weiteres Duplizieren vonFunktionseinheiten und Pipelinestufen ist zwar moglich,bringt aber wegen vorhandener Abhangigkeiten zwischenden Instruktionen kaum eine weitere Leistungssteigerung.Gegen eine weitere Erhohung der prozessoreigenen Taktra-te sprechen mehrere Grunde [36]:

• Ein Problem liegt darin, dass die Speicherzugriffsge-schwindigkeit nicht im gleichen Umfang wie die Pro-zessorgeschwindigkeit zunimmt, was zu einer Erhohungder Zyklenanzahl pro Speicherzugriff fuhrt. So brauch-te z.B. um 1990 ein Intel i486 fur einen Zugriff aufden Hauptspeicher zwischen 6 und 8 Maschinenzyklen,wahrend 2006 ein Intel Pentium Prozessor uber 220 Zy-klen benotigt. Die Speicherzugriffszeiten stellen dahereinen kritischen limitierenden Faktor fur eine weitereLeistungssteigerung dar.

• Zum Zweiten wird die Erhohung der Transistoranzahldurch eine erhohte Packungsdichte erreicht, mit der aberauch eine gesteigerte Warmeentwicklung pro Flachen-einheit verbunden ist. Diese wird zunehmend zum Pro-blem, da die notwendige Kuhlung entsprechend aufwen-diger wird.

• Zum Dritten wachst mit der Anzahl der Transistorenauch die prozessorinterne Leitungslange fur den Signal-transport, so dass die Signallaufzeit eine wichtige Rollespielt. Dies sieht man an folgender Berechnung: Ein mit3GHz getakteter Prozessor hat eine Zykluszeit von 0.33ns = 0.33 ·10−9 sec. In dieser Zeit kann ein Signal eine

Page 12: Multicore Parallele Programmierung Kng617

4 1 Kurzuberblick Multicore-Prozessoren

Entfernung von 0.33 ·10−9s·0.3 ·109m/s ≈ 0.1m zuruck-legen, wenn wir die Lichtgeschwindigkeit im Vakuum alsSignalgeschwindigkeit ansetzen. Je nach Ubergangsme-dium ist die Signalgeschwindigkeit sogar deutlich nied-riger. Damit konnen die Signale in einem Takt nur ei-ne relativ geringe Entfernung zurucklegen, so dass derLayout-Entwurf der Prozessorchips entsprechend gestal-tet werden muss.

Um eine weitere Leistungssteigerung der Prozessorenim bisherigen Umfang zu erreichen, setzen die Prozessor-hersteller auf eine explizite Parallelverarbeitung innerhalbeines Prozessors, indem mehrere logische Prozessoren voneinem physikalischen Prozessor simuliert werden oder meh-rere vollstandige, voneinander nahezu unabhangige Prozes-sorkerne auf einen Prozessorchip platziert werden. Der Ein-satz expliziter Parallelverarbeitung innerhalb eines Prozes-sors hat weitreichende Konsequenzen fur die Programmie-rung: soll ein Programm von der verfugbaren Leistung desMulticore-Prozessors profitieren, so muss es die verfugba-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-grammieransatze fur gemeinsamen Adressraum geeignet.

1.2 Parallelitat auf Prozessorebene

Explizite Parallelitat auf Prozessorebene wird durch eineentsprechende Architekturorganisation des Prozessorchipserreicht.

Eine Moglichkeit ist die oben erwahnte Platzierungmehrerer Prozessorkerne mit jeweils unabhangigen Ausfuh-rungseinheiten auf einem Prozessorchip, was als Multicore-

Page 13: Multicore Parallele Programmierung Kng617

1.2 Parallelitat auf Prozessorebene 5

Prozessor bezeichnet wird. Ein anderer Ansatz bestehtdarin, mehrere Kontrollflusse dadurch gleichzeitig auf ei-nem Prozessor oder Prozessorkern auszufuhren, dass derProzessor je nach Bedarf per Hardware zwischen den Kon-trollflussen umschaltet. Dies wird als simultanes Multi-threading (SMT) oder Hyperthreading (HT) bezeich-net [43].

Bei dieser Art der Parallelitat werden die Kontrollflusseoft als Threads bezeichnet. Dieser Begriff und die Unter-schiede zu Prozessen werden in den folgenden Abschnittennaher erlautert; zunachst reicht es aus, einen Thread alsKontrollfluss anzusehen, der parallel zu anderen Threadsdesselben Programms ausgefuhrt werden kann.

Simultanes Multithreading (SMT)

Simultanes Multithreading basiert auf dem Duplizieren desProzessorbereiches zur Ablage des Prozessorzustandes aufder Chipflache des Prozessors. Zum Prozessorzustand geho-ren die Benutzer- und Kontrollregister sowie der Interrupt-Controller mit seinen zugehorigen Registern. Damit verhaltsich der physikalische Prozessor aus der Sicht des Be-triebssystems und des Benutzerprogramms wie zwei lo-gische Prozessoren, denen Prozesse oder Threads zurAusfuhrung zugeordnet werden konnen. Diese konnen voneinem oder mehreren Anwendungsprogrammen stammen.

Jeder logische Prozessor legt seinen Prozessorzustand ineinem 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-

Page 14: Multicore Parallele Programmierung Kng617

6 1 Kurzuberblick Multicore-Prozessoren

ringfugige Vergroßerung der Chipflache. Fur zwei logischeProzessoren wachst z.B. fur einen Intel Xeon Prozessor dieerforderliche Chipflache um weniger als 5% [44, 67]. Diegemeinsamen Ressourcen des Prozessorchips werden denlogischen Prozessoren reihum zugeteilt, so dass die logi-schen Prozessoren simultan zur Verfugung stehen. Tretenbei einem logischen Prozessor Wartezeiten auf, konnen dieAusfuhrungs-Ressourcen dem anderen logischen Prozessorzugeordnet werden, so dass aus der Sicht des physikali-schen Prozessors eine verbesserte Nutzung der Ressourcengewahrleistet ist.

Untersuchungen zeigen, dass die verbesserte Nutzungder Ressourcen durch zwei logische Prozessoren je nach An-wendungsprogramm Laufzeitverbesserungen zwischen 15%und 30% bewirken kann [44]. Da alle Ressourcen des Chipsvon den logischen Prozessoren geteilt werden, ist beim Ein-satz von wesentlich mehr als zwei logischen Prozessoren furdie meisten Einsatzgebiete keine weitere signifikante Lauf-zeitverbesserung zu erwarten. Der Einsatz simultanen Mul-tithreadings wird daher voraussichtlich auf wenige logischeProzessoren beschrankt bleiben. Zum Erreichen einer Leis-tungsverbesserung durch den Einsatz der SMT-Technologieist es erforderlich, dass das Betriebssystem in der Lageist, die logischen Prozessoren anzusteuern. Aus Sicht einesAnwendungsprogramms ist es erforderlich, dass fur jedenlogischen Prozessor ein separater Thread zur Ausfuhrungbereitsteht, d.h. fur die Implementierung des Programmsmussen Techniken der parallelen Programmierung einge-setzt werden.

Multicore-Prozessoren

Neue Prozessorarchitekturen mit mehreren Prozessorker-nen auf einem Prozessorchip werden schon seit vielen Jah-

Page 15: Multicore Parallele Programmierung Kng617

1.2 Parallelitat auf Prozessorebene 7

ren als die vielversprechendste Technik zur weiteren Leis-tungssteigerung angesehen. Die Idee besteht darin, anstatteines Prozessorchips mit einer sehr komplexen internen Or-ganisation mehrere Prozessorkerne mit einfacherer Organi-sation auf dem Prozessorchip zu integrieren. Dies hat denzusatzlichen Vorteil, dass der Stromverbrauch des Prozes-sorchips dadurch reduziert werden kann, dass voruberge-hend ungenutzte Prozessorkerne abgeschaltet werden [27].

Bei Multicore-Prozessoren werden mehrere Prozessor-kerne auf einem Prozessorchip integriert. Jeder Prozessor-kern stellt fur das Betriebssystem einen separaten logischenProzessor mit separaten Ausfuhrungsressourcen dar, diegetrennt angesteuert werden mussen. Das Betriebssystemkann damit verschiedene Anwendungsprogramme parallelzueinander zur Ausfuhrung bringen. So konnen z.B. meh-rere Hintergrundanwendungen wie Viruserkennung, Ver-schlusselung und Kompression parallel zu Anwendungs-programmen des Nutzers ausgefuhrt werden [58]. Es istaber mit Techniken der parallelen Programmierung auchmoglich, ein rechenzeitintensives Anwendungsprogramm (et-wa aus dem Bereich der Computerspiele, der Bildverar-beitung oder naturwissenschaftlicher Simulationsprogram-me) auf mehreren Prozessorkernen parallel abzuarbeiten, sodass die Berechnungszeit im Vergleich zu einer Ausfuhrungauf einem Prozessorkern reduziert werden kann.

Damit konnen auch Standardprogramme wie Textver-arbeitungsprogramme oder Computerspiele zusatzliche, imHintergrund ablaufende Funktionalitaten zur Verfugungstellen, die parallel zu den Haupt-Funktionalitaten auf ei-nem separaten Prozessorkern durchgefuhrt werden und so-mit fur den Nutzer nicht zu wahrnehmbaren Verzogerun-gen fuhren. Fur die Koordination der innerhalb einer An-wendung ablaufenden unterschiedlichen Funktionalitaten

Page 16: Multicore Parallele Programmierung Kng617

8 1 Kurzuberblick Multicore-Prozessoren

mussen Techniken der parallelen Programmierung einge-setzt werden.

1.3 Architektur von Multicore-Prozessoren

Fur die Realisierung von Multicore-Prozessoren gibt es ver-schiedene Implementierungsvarianten, die sich in der An-zahl der Prozessorkerne, der Große und Anordnung der Ca-ches, den Zugriffmoglichkeiten der Prozessorkerne auf dieCaches und dem Einsatz von heterogenen Komponentenunterscheiden [37]. Dabei werden zur Zeit drei unterschied-liche Architekturmodelle eingesetzt, von denen auch Misch-formen auftreten konnen.

Hierarchisches Design

Kern Kern Kern

Cache/Speicher

Cache Cache

hierarchisches Design

Kern

Abbildung 1.1. Hierarchi-sches Design.

Bei einem hierarchischen De-sign teilen sich mehrere Pro-zessorkerne mehrere Caches,die in einer baumartigen Konfi-guration angeordnet sind, wo-bei die Große der Caches vonden Blattern zur Wurzel steigt.Die Wurzel reprasentiert dieVerbindung zum Hauptspei-cher. So kann z.B. jeder Pro-zessorkern einen separaten L1-Cache haben, sich aber mitanderen Prozessorkernen einenL2-Cache teilen. Alle Prozes-sorkerne konnen auf den ge-meinsamen externen Haupt-speicher zugreifen, was eine

Page 17: Multicore Parallele Programmierung Kng617

1.3 Architektur von Multicore-Prozessoren 9

dreistufige Hierarchie ergibt. Dieses Konzept kann auf meh-rere Stufen erweitert werden und ist in Abbildung 1.1fur drei Stufen veranschaulicht. Zusatzliche Untersyste-me konnen die Caches einer Stufe miteinander verbinden.Ein hierarchisches Design wird typischerweise fur Server-Konfigurationen verwendet.

Ein Beispiel fur ein hierarchisches Design ist der IBMPower5 Prozessor, der zwei 64-Bit superskalare Prozessor-kerne enthalt, von denen jeder zwei logische Prozessorendurch Einsatz von SMT simuliert. Jeder Prozessorkern hateinen separaten L1-Cache (fur Daten und Programme ge-trennt) und teilt sich mit dem anderen Prozessorkern einenL2-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 dieSun UltraSPARC T1 (Niagara) Prozessoren.

Pipeline-Design

Abbildung 1.2. Pipeline-Design.

Bei einem Pipeline-Designwerden die Daten durch meh-rere Prozessorkerne schrittwei-se weiterverarbeitet, bis sievom letzten Prozessorkern imSpeichersystem 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 undX11 Prozessoren von Xelera-tor zur Verarbeitung von Netz-werkpaketen in Hochleistungs-Routern. Der Xelerator X10q,

Page 18: Multicore Parallele Programmierung Kng617

10 1 Kurzuberblick Multicore-Prozessoren

eine Variante des X10, enthalt z.B. 200 separate Prozes-sorkerne, die in einer logischen linearen Pipeline mitein-ander verbunden sind. Die Pakete werden dem Prozessoruber mehrere Netzwerkschnittstellen zugefuhrt und danndurch die Prozessorkerne schrittweise verarbeitet, wobei je-der Prozessorkern einen Schritt ausfuhrt. Die X11 Netz-werkprozessoren haben bis zu 800 Pipeline-Prozessorkerne.

Netzwerkbasiertes Design

Abbildung 1.3. Netzwerk-basiertes Design.

Bei einem netzwerkbasier-ten Design sind die Pro-zessorkerne und ihre lokalenCaches oder Speicher uberein Verbindungsnetzwerk mitden anderen Prozessorkernendes Chips verbunden, so dassder gesamte Datentransfer zwi-schen den Prozessorkernen uberdas Verbindungsnetzwerk lauft,vgl. Abbildung 1.3. Der Ein-satz eines prozessorinternenNetzwerkes ist insbesonderedann sinnvoll, wenn eine Viel-zahl von Prozessorkernen ver-wendet werden soll. Ein netz-

werkorientiertes Design wird z.B. fur den Intel Teraflop-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

Page 19: Multicore Parallele Programmierung Kng617

1.3 Architektur von Multicore-Prozessoren 11

seit 2005 bieten viele Hardwarehersteller Prozessoren mitzwei oder mehr Kernen an. Ab Ende 2006 liefert IntelQuadcore-Prozessoren und ab 2008 wird mit der Auslie-ferung von Octcore-Prozessoren gerechnet. IBM bietet mitder Cell-Architektur einen Prozessor mit acht spezialisier-ten Prozessorkernen, vgl. Abschnitt 1.4. Der seit Dezember2005 ausgelieferte UltraSPARC T1 Niagara Prozessor vonSun hat bis zu acht Prozessorkerne, von denen jeder durchden Einsatz von simultanem Multithreading, das von Sunals CoolThreads-Technologie bezeichnet wird, vier Thre-ads simultan verarbeiten kann. Damit kann ein UltraS-PARC T1 bis zu 32 Threads simultan ausfuhren. Das fur2008 angekundigte Nachfolgemodell des Niagara-Prozessors(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 Prozessorkerneenthalt, die als 8×10-Gitter organisiert sind. Jeder Prozes-sorkern kann Floating-Point-Operationen verarbeiten undenthalt neben einem lokalen Cachespeicher auch einen Rou-ter zur Realisierung des Datentransfers zwischen den Pro-zessorkernen und dem Hauptspeicher. Zusatzlich kann einsolcher Prozessor spezialisierte Prozessorkerne fur die Ver-arbeitung von Videodaten, graphischen Berechnungen undzur Verschlusselung von Daten enthalten. Je nach Einsatz-gebiet kann die Anzahl der spezialisierten Prozessorkernevariiert werden.

Ein wesentlicher Bestandteil eines Prozessors mit ei-ner Vielzahl von Prozessorkernen ist ein effizientes Verbin-

Page 20: Multicore Parallele Programmierung Kng617

12 1 Kurzuberblick Multicore-Prozessoren

dungsnetzwerk auf dem Prozessorchip, das an eine variableAnzahl von Prozessorkernen angepasst werden kann, denAusfall einzelner Prozessorkerne toleriert und bei Bedarfdas Abschalten einzelner Prozessorkerne erlaubt, falls diesefur die aktuelle Anwendung nicht benotigt werden. Ein sol-ches Abschalten ist insbesondere zur Reduktion des Strom-verbrauchs sinnvoll.

Fur eine effiziente Nutzung der Prozessorkerne ist ent-scheidend, dass die zu verarbeitenden Daten schnell zu denProzessorkernen transportiert werden konnen, so dass diesenicht auf die Bereitstellung der Daten warten mussen. Dazusind ein leistungsfahiges Speichersystem und I/O-Systemerforderlich. Das Speichersystem setzt private L1-Cachesein, auf die nur von jeweils einem Prozessorkern zugegriffenwerden kann, sowie gemeinsame, evtl. aus mehreren Stufenbestehende L2-Caches ein, die Daten verschiedener Prozes-sorkerne enthalten. Fur einen Prozessorchip mit Dutzendenvon Prozessorkernen muss voraussichtlich eine weitere Stufeim Speichersystem eingesetzt werden [27]. Das I/O-Systemmuss 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 Unterstutzung fur Floating-Point-Berechnungen bietet und somit uberwiegend fur den Ein-satz im Serverbereich, wie Web-Server oder Applikations-Server, geeignet ist. Fur eine detailliertere Behandlung derArchitektur von Multicore-Prozessoren und weiterer Bei-spiele verweisen wir auf [10, 28].

Page 21: Multicore Parallele Programmierung Kng617

1.4 Beispiele 13

Tabelle 1.1. Uberblick uber verschiedene Multicore-Prozessoren,vgl. auch [28].

Intel IBM AMD SunProzessor Core 2 Duo Power 5 Opteron T1

Prozessorkerne 2 2 2 8Instruktionen 4 4 3 1pro ZyklusSMT nein ja nein jaL1-Cache I/D 32/32 64/32 64/64 16/8in KB per coreL2-Cache 4 MB 1.9 MB 1 MB 3 MB

shared shared per core sharedTaktrate (GHz) 2.66 1.9 2.4 1.2Transistoren 291 Mio 276 Mio 233 Mio 300 MioStromverbrauch 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 Core2 Duo-Architektur und dem IBM Cell-Prozessor.

Intel Core 2 Prozessor

Intel Core 2 bezeichnet eine Familie von Intel-Prozessorenmit ahnlicher Architektur. Die Intel Core-Architektur ba-siert auf einer Weiterentwicklung der Pentium M Prozesso-ren, die viele Jahre im Notebookbereich eingesetzt wurden.Die neue Architektur lost die bei den Pentium 4 Prozesso-ren noch eingesetzte NetBurst-Architektur ab. SignifikanteMerkmale der neuen Architektur sind:

Page 22: Multicore Parallele Programmierung Kng617

14 1 Kurzuberblick Multicore-Prozessoren

• eine drastische Verkurzung der internen Pipelines (ma-ximal 14 Stufen anstatt maximal 31 Stufen bei derNetBurst-Architektur), damit verbunden

• eine Reduktion der Taktrate und damit verbunden auch• eine deutliche Reduktion des Stromverbrauchs: die Re-

duktion des Stromverbrauches wird auch durch einePower-Management-Einheit unterstutzt, die das zeit-weise Ausschalten ungenutzter Prozessorteile ermoglicht[48] und

• die Unterstutzung neuer Streaming-Erweiterungen (Stre-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 unabhangigen Prozessorkernen in 65nm-Technologie ge-fertigt. Im Folgenden wird der Core 2 Duo Prozessor kurzbeschrieben [24]. Die Core 2 Quad Prozessoren haben einenahnlichen Aufbau, enthalten aber 4 statt 2 Prozessorkerne.

Da die Core 2 Prozessoren auf der Technik des PentiumM Prozessors basieren, unterstutzen sie kein Hyperthrea-ding. Die allgemeine Struktur der Core 2 Duo Architekturist in Abb. 1.4 wiedergegeben. Der Prozessorchip enthaltzwei unabhangige Prozessorkerne, von denen jeder separateL1-Caches anspricht; diese sind fur Instruktionen (32K) undDaten (32K) getrennt realisiert. Der L2-Cache (4 MB) istdagegen nicht exklusiv und wird von beiden Prozessorker-nen gemeinsam fur Instruktionen und Daten genutzt. AlleZugriffe von den Prozessorkernen und vom externen Bus aufden L2-Cache werden von einem L2-Controller behandelt.Fur die Sicherstellung der Koharenz der auf den verschie-denen Stufen der Speicherhierarchie abgelegten Daten wirdein MESI-Protokoll (Modified, Exclusive, Shared, Invalid)verwendet, vgl. [17, 47, 59] fur eine detaillierte Erklarung.Alle Daten- und I/O-Anfragen zum oder vom externen Bus

Page 23: Multicore Parallele Programmierung Kng617

1.4 Beispiele 15

(Front Side Bus) werden uber einen Bus-Controller gesteu-ert.

Power−Management−Controller

L2−Cache mit Controller (shared)

Bus−Interface und −Controller

Core 0 Core 1

FrontSideBus

Ausführungs−Ressourcen

L1−Caches (I/O)

Architektur−Ressourcen

Ausführungs−Ressourcen

L1−Caches (I/O)

Architektur−Ressourcen

Cache−Controller Cache−Controller

Core 2 Duo Prozessor

Abbildung 1.4. Uberblick Core 2 Duo Architektur.

Ein wichtiges Element ist die Kontrolleinheit fur denStromverbrauch des Prozessors (Power Management Con-troller) [48], die den Stromverbrauch des Prozessorchipsdurch Reduktion der Taktrate der Prozessorenkerne oderdurch Abschalten (von Teilen) des L2-Caches reduzierenkann.

Jeder Prozessorkern fuhrt einen separaten Strom vonInstruktionen aus, der sowohl Berechnungs- als auch Spei-cherzugriffsinstruktionen (load/store) enthalten kann. Da-bei kann jeder der Prozessorkerne bis zu vier Instruktionengleichzeitig verarbeiten. Die Prozessorkerne enthalten sepa-rate Funktionseinheiten fur das Laden bzw. Speichern von

Page 24: Multicore Parallele Programmierung Kng617

16 1 Kurzuberblick Multicore-Prozessoren

Daten, die Ausfuhrung von Integeroperationen (durch ei-ne ALU, arithmetic logic unit), Floating-Point-Operationensowie SSE-Operationen. Instruktionen konnen aber nurdann parallel zueinander ausgefuhrt werden, wenn keineAbhangigkeiten zwischen ihnen bestehen. Fur die Steue-rung der Ausfuhrung werden komplexe Schedulingverfah-ren eingesetzt, die auch eine Umordnung von Instruktionen(out-of-order execution) erlauben, um eine moglichst guteAusnutzung der Funktionseinheiten zu verwirklichen [28].

MikrocodeROM

ALUBranchMMX/SSEFPMove

ALU

FPMove

FPAddMMX/SSE

ALUFPMulMMX/SSEFPMove

L1−Datencache

(shared)Cache

L2−

Instruktionsschlange

Instruktions−Scheduler

Umordnungspuffer

Laden von Instruktionen

Dekodiereinheit

Register−Umbenennung und −Allokierung

StoreLoad

Speicherzugriffspuffer

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 ladt x86-Instruktionenin eine Instruktionsschlange, auf die die Dekodiereinheitzugreift und die Instruktionen in Mikroinstruktionen zer-

Page 25: Multicore Parallele Programmierung Kng617

1.4 Beispiele 17

legt. Fur komplexere x86-Instruktionen werden die zu-gehorigen Mikroinstruktionen uber einen ROM-Speichergeladen. Die Mikroinstruktionen werden vom Instruktions-Scheduler freien Funktionseinheiten zugeordnet, wobei dieInstruktionen in einer gegenuber dem ursprunglichen Pro-grammcode geanderten Reihenfolge abgesetzt werden kon-nen. Alle Speicherzugriffsoperationen werden uber den L1-Datencache abgesetzt, der Integer-und Floating-Point-Da-ten enthalt.

Fur Ende 2007 bzw. Anfang 2008 sollen Intel Core 2-Prozessoren mit verbesserter Core-Architektur eingefuhrtwerden (Codename Penryn). Voraussichtlich fur Ende 2008ist eine neue Generation von Intel-Prozessoren geplant, dieauf einer neuen Architektur basiert (Codename Nehalem).Diese neuen Prozessoren sollen neben mehreren Prozessor-kernen (zu Beginn acht) auch einen Graphikkern und einenSpeichercontroller auf einem Prozessorchip integrieren. Dieneuen Prozessoren sollen auch wieder die SMT-Technik (si-multanes Multithreading) unterstutzen, so dass auf jedemProzessorkern gleichzeitig zwei Threads ausgefuhrt werdenkonnen. Diese Technik wurde teilweise fur Pentium 4 Pro-zessoren verwendet, nicht jedoch fur die Core 2 Duo undQuad Prozessoren.

IBM Cell-Prozessor

Der Cell-Prozessor wurde von IBM in Zusammenarbeitmit Sony und Toshiba entwickelt. Der Prozessor wird u.a.von Sony in der Spielekonsole PlayStation 3 eingesetzt,siehe [39, 34] fur ausfuhrlichere Informationen. Der Cell-Prozessor enthalt ein Power Processing Element (PPE) und8 Single-Instruction Multiple-Datastream (SIMD) Prozesso-ren. Das PPE ist ein konventioneller 64-Bit-Mikroprozessorauf der Basis der Power-Architektur von IBM mit relativ

Page 26: Multicore Parallele Programmierung Kng617

18 1 Kurzuberblick Multicore-Prozessoren

einfachem Design: der Prozessor kann pro Takt zwei In-struktionen absetzen und simultan zwei unabhangige Thre-ads ausfuhren. Die einfache Struktur hat den Vorteil, dasstrotz hoher Taktrate eine geringe Leistungsaufnahme re-sultiert. Fur den gesamten Prozessor ist bei einer Taktratevon 3.2 GHz nur eine Leistungsaufnahme von 60-80 Watterforderlich.

Auf der Chipflache des Cell-Prozessors sind neben demPPE acht SIMD-Prozessoren integriert, die als SPE (Syn-ergetic Processing Element) bezeichnet werden. Jedes SPEstellt einen unabhangigen Vektorprozessor mit einem 256KBgroßen lokalem SRAM-Speicher dar, der als Local Store(LS) bezeichnet wird. Das Laden von Daten in den LS unddas Zuruckspeichern von Resultaten aus dem LS in denHauptspeicher muss per Software erfolgen.

Jedes SPE enthalt 128 128-Bit-Register, in denen dieOperanden von Instruktionen abgelegt werden konnen. Daauf 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 fuhrt damit zu ei-ner geringen mittleren Speicherzugriffszeit. Jedes SPE hatvier Floating-Point-Einheiten (32 Bit) und vier Integer-Einheiten. Zahlt man eine Multiply-Add-Instruktion alszwei Operationen, kann jedes SPE bei 3.2 GHz Taktrate proSekunde uber 25 Milliarden Floating-Point-Operationen(25.6 GFlops) und uber 25 Milliarden Integer-Operationen(25.6 Gops) ausfuhren. Da ein Cell-Prozessor acht SPEenthalt, fuhrt dies zu einer maximalen Performance vonuber 200 GFlops, wobei die Leistung des PPE noch nichtberucksichtigt wurde. Eine solche Leistung kann allerdingsnur bei guter Ausnutzung der LS-Speicher und effizienterZuordnung von Instruktionen an Funktionseinheiten derSPE erreicht werden. Zu beachten ist auch, dass sich dieseAngabe auf 32-Bit Floating-Point-Zahlen bezieht. Der Cell-

Page 27: Multicore Parallele Programmierung Kng617

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 undzur Vereinfachung des Schedulings verwenden die SPEs in-tern keine SMT-Technik.

Die zentrale Verbindungseinheit des Cell-Prozessors istein Bussystem, der sogenannte Element Interconnect Bus(EIB). Dieser besteht aus vier unidirektionalen Ringver-bindungen, die eine Wortbreite von 16 Bytes haben undmit der halben Taktrate des Prozessors arbeiten. Zwei derRinge werden in entgegengesetzter Richtung zu den ande-ren beiden Ringe betrieben, so dass die maximale Latenzim schlechtesten Fall durch einen halben Ringdurchlauf be-stimmt wird. Fur den Transport von Daten zwischen be-nachbarten Ringelementen konnen maximal drei Transfer-operationen simultan durchgefuhrt werden, fur den Zyklusdes Prozessors ergibt dies 16 · 3/2 = 24 Bytes pro Zyklus.Fur die vier Ringverbindungen ergibt dies eine maxima-le Transferrate von 96 Bytes pro Zyklus, woraus bei einerTaktrate von 3.2 GHz eine maximale Transferrate von uber300 GBytes/Sekunde resultiert. Abbildung 1.6 zeigt einenschematischen Aufbau des Cell-Prozessors mit den bisherbeschriebenen Elementen sowie dem Speichersystem (Me-mory Interface Controller, MIC) und dem I/O-System (BusInterface Controller, BIC). Das Speichersystem unterstutztdie XDR-Schnittstelle von Rambus. Das I/O-System un-terstutzt das Rambus RRAC (Redwood Rambus AccessCell) Protokoll.

Zum Erreichen einer guten Leistung ist es wichtig, dieSPEs des Cell-Prozessors effizient zu nutzen. Dies kann furspezialisierte Programme, wie z.B. Videospiele, durch di-rekte Verwendung von SPE-Assembleranweisungen erreichtwerden. Da dies fur die meisten Anwendungsprogramme

Page 28: Multicore Parallele Programmierung Kng617

20 1 Kurzuberblick Multicore-Prozessoren

DualXDR

16B/Zyklus

SPU SPU SPU SPU SPU SPU SPU

LSLSLSLSLSLSLSLS

EIB (bis 96 B/Zyklus)

64−Bit Power Architektur RRAC I/O

Synergetic Processing Elements

SPU

MIC BIC

PPU L1

L2

Abbildung 1.6. Schematischer Aufbau des Cell-Prozessors.

zu aufwendig ist, werden fur das Erreichen einer gutenGesamtleistung eine effektive Compilerunterstutzung sowiedie Verwendung spezialisierter Programmbibliotheken z.B.zur Verwaltung von Taskschlangen wichtig sein.

Page 29: Multicore Parallele Programmierung Kng617

2

Konzepte parallelerProgrammierung

Die Leistungsverbesserung der Generation der Multicore-Prozessoren wird technologisch durch mehrere Prozessor-kerne auf einem Chip erreicht. Im Gegensatz zu bisherigenLeistungsverbesserungen bei Prozessoren hat diese Tech-nologie jedoch Auswirkungen auf die Softwareentwicklung:Konnten bisherige Verbesserungen der Prozessorhardwarezu Leistungsgewinnen bei existierenden (sequentiellen) Pro-grammen fuhren, ohne dass die Programme geandert wer-den mussten, so ist zur vollen Ausnutzung der Leistungder Multicore-Prozessoren ein Umdenken hin zur paralle-len Programmierung notwendig [62].

Parallele Programmiertechniken sind seit vielen Jahrenim 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 kunftige Allgegenwartigkeit der Multicore-Prozessoren ein Ausbreiten paralleler Programmiertechni-ken in alle Bereiche der Softwareentwicklung erwartet wirdund diese damit zum Rustzeug eines jeden Softwareentwick-lers gehoren werden.

Page 30: Multicore Parallele Programmierung Kng617

22 2 Konzepte paralleler Programmierung

Ein wesentlicher Schritt in der Programmierung vonMulticore-Prozessoren ist das Bereitstellen mehrerer Be-rechnungsstrome, die auf den Kernen eines Multicore-Pro-zessors simultan, also gleichzeitig, abgearbeitet werden. Zu-nachst ist die rein gedankliche Arbeit durchzufuhren, eineneinzelnen Anwendungsalgorithmus in solche Berechnungs-strome zu zerlegen, was eine durchaus langwierige, schwie-rige und kreative Aufgabe sein kann, da es eben sehr vieleMoglichkeiten gibt, dies zu tun, und insbesondere korrekteund effiziente Software resultieren soll.

Zur Erstellung der parallelen Software sind einige Grund-begriffe und -kenntnisse hilfreich:

• Wie wird beim Entwurf eines parallelen Programmesvorgegangen?

• Welche Eigenschaften der parallelen Hardware sollen zuGrunde gelegt werden?

• Welches parallele Programmiermodell soll genutzt wer-den?

• Wie kann der Leistungsvorteil des parallelen Programmsgegenuber dem sequentiellen bestimmt werden?

• Welche parallele Programmierumgebung oder -sprachesoll genutzt werden?

2.1 Entwurf paralleler Programme

Die Grundidee der parallelen Programmierung besteht dar-in, mehrere Berechnungsstrome zu erzeugen, die gleichzei-tig, also parallel, ausgefuhrt werden konnen und durch ko-ordinierte Zusammenarbeit eine gewunschte Aufgabe er-ledigen. Liegt bereits ein sequentielles Programm vor, sospricht man auch von der Parallelisierung eines Program-mes.

Page 31: Multicore Parallele Programmierung Kng617

2.1 Entwurf paralleler Programme 23

Zur Erzeugung der Berechnungsstrome wird die aus-zufuhrende Aufgabe in Teilaufgaben zerlegt, die auch Tasksgenannt werden. Tasks sind die kleinsten Einheiten der Par-allelitat. Beim Entwurf der Taskstruktur eines Program-mes mussen Daten- und Kontrollabhangigkeiten beachtetund eingeplant werden, um ein korrektes paralleles Pro-gramm zu erhalten. Die Große der Tasks wird Granu-laritat genannt. Fur die tatsachliche parallele Abarbei-tung werden die Teilaufgaben in Form von Threads oderProzessen auf physikalische Rechenressourcen abgebildet.Die Rechenressourcen konnen Prozessoren eines Paral-lelrechners, aber auch die Prozessorkerne eines Multicore-Prozessors sein.

Die Zuordnung von Tasks an Prozesse oder Threadswird auch als Scheduling bezeichnet. Dabei kann manzwischen statischem Scheduling, bei dem die Zuteilungbeim Start des Programms festgelegt wird, und dynami-schem Scheduling, bei dem die Zuteilung wahrend derAbarbeitung des Programms erfolgt, unterscheiden. DieAbbildung von Prozessen oder Threads auf Prozessorker-ne, auch Mapping genannt, kann explizit im Programmbzw. durch das Betriebssystem erfolgen. Abbildung 2.1zeigt eine Veranschaulichung.

Software mit mehreren parallel laufenden Tasks gibt esin vielen Bereichen schon seit Jahren. So bestehen Server-anwendungen haufig aus mehreren Threads oder Prozes-sen. Ein typisches Beispiel ist ein Webserver, der mit ei-nem Haupt-Thread HTTP-Anfragenachrichten von belie-bigen Clients (Browsern) entgegennimmt und fur jede ein-treffende Verbindungsanfrage eines Clients einen separa-ten Thread erzeugt. Dieser Thread behandelt alle von die-sem Client eintreffenden HTTP-Anfragen und schickt diezugehorigen HTTP-Antwortnachrichten uber eine Client-spezifische TCP-Verbindung. Wird diese TCP-Verbindung

Page 32: Multicore Parallele Programmierung Kng617

24 2 Konzepte paralleler Programmierung

T1

T2

T3ZerlegungTask−

Schedu−ling

T1 T2

T3

Tasks Threads

Thread−Zuordnung

Mapping P1

P2

Prozessor−Kerne

Abbildung 2.1. Veranschaulichung der typischen Schritte zurParallelisierung eines Anwendungsalgorithmus. Der Algorithmuswird in der Zerlegungsphase in Tasks mit gegenseitigen Abhangig-keiten aufgespalten. Diese Tasks werden durch das SchedulingThreads zugeordnet, die auf Prozessorkerne abgebildet werden.

geschlossen, wird auch der zugehorige Thread beendet.Durch dieses Vorgehen kann ein Webserver gleichzeitig vie-le ankommende Anfragen nebenlaufig erledigen oder aufverfugbaren Rechenressourcen parallel bearbeiten. Fur Web-server mit vielen Anfragen (wie google oder ebay) werdenentsprechende Plattformen mit vielen Rechenressourcen be-reitgehalten.

Abarbeitung paralleler Programme

Fur die parallele Abarbeitung der Tasks bzw. Threads oderProzesse gibt es verschiedene Ansatze, vgl. z.B. auch [3]:

• Multitasking: Multitasking-Betriebssysteme arbeitenmehrere Threads oder Prozesse in Zeitscheiben auf dem-selben Prozessor ab. Hierdurch konnen etwa die Latenz-zeiten von I/O-Operationen durch eine verschachtelteAbarbeitung der Tasks uberdeckt werden. Diese Form

Page 33: Multicore Parallele Programmierung Kng617

2.1 Entwurf paralleler Programme 25

der Abarbeitung mehrerer Tasks wird als Nebenlaufig-keit (Concurrency) bezeichnet; mehrere Tasks werdengleichzeitig bearbeitet, aber nur eine Task macht zu ei-nem bestimmten Zeitpunkt einen tatsachlichen Rechen-fortschritt. Eine simultane parallele Abarbeitung findetalso nicht statt.

• Multiprocessing: Die Verwendung mehrerer physi-kalischer Prozessoren macht eine tatsachliche paralle-le Abarbeitung mehrerer Tasks moglich. Bedingt durchdie hardwaremaßige Parallelitat mit mehreren physika-lischen Prozessoren kann jedoch ein nicht unerheblicherzeitlicher Zusatzaufwand (Overhead) entstehen.

• Simultanes Multithreading (SMT): Werden meh-rere logische Prozessoren auf einem physikalischen Pro-zessor ausgefuhrt, so konnen die Hardwareressourcen ei-nes Prozessors besser genutzt werden und es kann eineteilweise beschleunigte Abarbeitung von Tasks erfolgen,vgl. Kap. 1. Bei zwei logischen Prozessoren sind Leis-tungssteigerungen durch Nutzung von Wartezeiten ei-nes Threads fur die Berechnungen des anderen Threadsum bis zu 30 % moglich.

• Chip-Multiprocessing: Der nachste Schritt ist nun,die Idee der Threading-Technologie auf einem Chip mitdem Multiprocessing zu verbinden, was durch Multicore-Prozessoren moglich ist. Die Programmierung von Mul-ticore-Prozessoren vereint das Multiprocessing mit demsimultanen Multithreading in den Sinne, dass Multi-threading-Programme nicht nebenlaufig sondern tat-sachlich parallel auf einen Prozessor abgearbeitet wer-den. Dadurch sind im Idealfall Leistungssteigerungenbis zu 100 % fur einen Dualcore-Prozessor moglich.

Fur die Programmierung von Multicore-Prozessoren wer-den Multithreading-Programme eingesetzt. Obwohl viele

Page 34: Multicore Parallele Programmierung Kng617

26 2 Konzepte paralleler Programmierung

moderne Programme bereits Multithreading verwenden,gibt es doch Unterschiede, die gegenuber der Programmie-rung von Prozessoren mit simultanem Multithreading zubeachten sind:

• Einsatz zur Leistungsverbesserung: Die Leistungsver-besserungen von SMT-Prozessoren wird meistens zurVerringerung der Antwortzeiten fur den Nutzer ein-gesetzt, indem etwa ein Thread zur Beantwortung ei-ner oder mehrerer Benutzeranfragen abgespalten undnebenlaufig ausgefuhrt wird. In Multicore-Prozessorenhingegen wird die gesamte Arbeit eines Programmesdurch Partitionierung auf die einzelnen Kerne verteiltund gleichzeitig abgearbeitet.

• Auswirkungen des Caches: Falls jeder Kern eines Mul-ticore-Prozessors einen eigenen Cache besitzt, so kannes zu dem beim Multiprocessing bekannten False Sha-ring kommen. Bei False Sharing handelt es sich um dasProblem, dass zwei Kerne gleichzeitig auf Daten arbei-ten, die zwar verschieden sind, jedoch in derselben Ca-chezeile liegen. Obwohl die Daten also unabhangig sind,wird die Cachezeile im jeweils anderen Kern als ungultigmarkiert, wodurch es zu Leistungsabfall kommt.

• Thread-Prioritaten: Bei der Ausfuhrung von Multithrea-ding-Programmen auf Prozessoren mit nur einem Kernwird immer der Thread mit der hochsten Prioritat zu-erst bearbeitet. Bei Prozessoren mit mehreren Kernenkonnen jedoch auch Threads mit unterschiedlichen Prio-ritaten gleichzeitig abgearbeitet werden, was zu durch-aus unterschiedlichen Resultaten fuhren kann.

Diese Beispiele zeigen, dass fur den Entwurf von Multi-threading-Programmen fur Multicore-Prozessoren nicht nurdie Techniken der Threadprogrammierung gebraucht wer-den, sondern Programmiertechniken, Methoden und Design-

Page 35: Multicore Parallele Programmierung Kng617

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 denenein Programm in Form mehrerer Berechnungsstrome abge-arbeitet wird. Die Formen paralleler Hardware reichen alsovon herkommlichen Parallelrechnern bis hin zu parallelenRechenressourcen auf einem Chip, wie z.B. bei Multicore-Prozessoren. Eine erste Klassifizierung solcher parallelerHardware hat bereits Flynn in der nach ihm benanntenFlynnschen Klassifikation gegeben [23]. Diese Klassi-fikation unterteilt parallele Hardware in vier Klassen mitunterschiedlichen Daten- und Kontrollflussen:

• Die SISD (Single Instruction, Single Data) Rechner be-sitzen eine Rechenressource, einen Datenspeicher undeinen Programmspeicher, entsprechen also dem klassi-schen von-Neumann-Rechner der sequentiellen Pro-grammierung.

• Die MISD (Multiple Instruction, Single Data) Rechnerstellen mehrere Rechenressourcen, aber nur einen Pro-grammspeicher bereit. Wegen der geringen praktischenRelevanz spielt diese Klasse keine wesentliche Rolle.

• Die SIMD (Single Instruction, Multiple Data) Rech-ner bestehen aus mehreren Rechenressourcen mit jeweilsseparatem Zugriff auf einen Datenspeicher, aber nur ei-nem Programmspeicher. Jede Ressource fuhrt dieselbenInstruktionen aus, die aus dem Programmspeicher gela-den werden, wendet diese aber auf unterschiedliche Da-ten an. Fur diese Klasse wurden in der VergangenheitParallelrechner konstruiert und genutzt.

Page 36: Multicore Parallele Programmierung Kng617

28 2 Konzepte paralleler Programmierung

• Die MIMD (Multiple Instruction, Multiple Data) Rech-ner sind die allgemeinste Klasse und zeichnen sich durchmehrere Rechenressourcen mit jeweils separatem Zugriffauf einen Datenspeicher und jeweils lokalen Programm-speichern aus. Jede Rechenressource kann also unter-schiedliche Instruktionen auf unterschiedlichen Datenverarbeiten.

Zur Klasse der MIMD-Rechner gehoren viele der heu-te aktuellen Parallelrechner, Cluster von PCs aber auchMulticore-Prozessoren, wobei die einzelnen Prozessoren,die PCs des Clusters oder die Kerne auf dem Chip ei-nes Multicore-Prozessors die jeweiligen Rechenressourcenbilden. Dies zeigt, dass die Flynnsche Klassifizierung furdie heutige Vielfalt an parallelen Rechenressourcen zu grobist und weitere Unterteilungen fur den Softwareentwicklernutzlich sind. Eine der weiteren Unterschiede der Hardwarevon MIMD-Rechnern ist die Speicherorganisation, die sichauf den Zugriff der Rechenressourcen auf die Daten einesProgramms 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 benotigt,der zu einer anderen Rechenressource lokal ist, so werdenProgrammiermechanismen, wie z. B. Kommunikation uberein Verbindungsnetzwerk, eingesetzt. Clustersysteme, aberauch viele Parallelrechner gehoren in diese Klasse.

Rechner mit gemeinsamem Speicher bestehen ausmehreren Rechenressourcen und einem globalen oder ge-meinsamen Speicher, auf den alle Rechenressourcen uberein Verbindungsnetzwerk auf Daten zugreifen konnen. Da-durch kann jede Rechenressource die gesamten Daten des

Page 37: Multicore Parallele Programmierung Kng617

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 desSpeichers in verteilten und gemeinsamen Speicher kann furden Programmierer in Form privater oder gemeinsamer Va-riable sichtbar und nutzbar sein. Es ist prinzipiell jedochmit Hilfe entsprechender Softwareunterstutzung moglich,das Programmieren mit gemeinsamen Variablen (shared va-riables) auch auf physikalisch verteiltem Speicher bereitzu-stellen. Ebenso kann die Programmierung mit verteiltemAdressraum und Kommunikation auf einem physikalischgemeinsamen Speicher durch zusatzliche Software moglichsein. Dies ist nur ein Beispiel dafur, dass die gegebene Hard-ware nur ein Teil dessen ist, was dem Softwareentwickler alsSicht auf ein paralleles System dient.

2.3 Parallele Programmiermodelle

Der Entwurf eines parallelen Programmes basiert immerauch 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 ausder Sicht, die sich dem Softwareentwickler durch System-software wie Betriebssystem, parallele Programmierspracheoder -bibliothek, Compiler und Laufzeitbibliothek bietet.Entsprechend viele durchaus unterschiedliche parallele Pro-grammiermodelle bieten sich dem Softwareentwickler an.Folgende Kriterien erlauben jedoch eine systematische Her-

Page 38: Multicore Parallele Programmierung Kng617

30 2 Konzepte paralleler Programmierung

angehensweise an diese Vielfalt der Programmiermodelle[59]:

• Auf welcher Ebene eines Programmes sollen paralleleProgrammteile ausgefuhrt werden? (z.B. Instruktions-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 erzeugteMenge von Prozessen oder etwa Tasks, die dynamischerzeugt und zugeordnet werden)

• Wie erfolgt die Abarbeitung der parallelen Programm-teile im Verhaltnis zueinander? (SIMD oder SPMD (Sin-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 konnen genutztwerden? (z.B. Barrier-Synchronisation oder Sperrme-chanismen)

Ebenen der Parallelitat

Unabhangige und damit parallel abarbeitbare Aufgabenkonnen auf sehr unterschiedlichen Ebenen eines Programmsauftreten, wobei fur die Parallelisierung eines Programmesmeist jeweils nur eine Ebene genutzt wird.

• Parallelitat auf Instruktionsebene kann ausgenutztwerden, falls zwischen zwei Instruktionen keine Daten-abhangigkeit besteht. Diese Form der Parallelitat kanndurch Compiler auf superskalaren Prozessoren einge-setzt werden.

Page 39: Multicore Parallele Programmierung Kng617

2.3 Parallele Programmiermodelle 31

• Bei der Parallelitat auf Anweisungsebene werdenmehrere Anweisungen auf denselben oder verschiede-nen Daten parallel ausgefuhrt. Eine Form ist die Da-tenparallelitat, bei der Datenstrukturen wie Felder inTeilbereiche unterteilt werden, auf denen parallel zuein-ander dieselben Operationen ausgefuhrt werden. Die-se Arbeitsweise wird im SIMD Modell genutzt, in demin jedem Schritt die gleiche Anweisung auf evtl. unter-schiedlichen Daten ausgefuhrt wird.

• Bei der Parallelitat auf Schleifenebene werden un-terschiedliche Iterationen einer Schleifenanweisung par-allel zueinander ausgefuhrt. Besondere Auspragungensind 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 fuhrt alle Anweisungeneiner Schleifeniteration unabhangig vor den anderenSchleifeniterationen aus. Je nach Datenabhangigkeitenzwischen den Schleifeniterationen kann es durch die Par-allelitat auf Schleifenebene zu unterschiedlichen Ergeb-nissen kommen als bei der sequentiellen Abarbeitungder Schleifenrumpfe. Als parallele Schleife wird eineSchleife bezeichnet, deren Schleifenrumpfe keine Daten-abhangigkeit aufweisen und somit eine parallele Abar-beitung zum gleichen Ergebnis fuhrt wie die sequentielleAbarbeitung. Parallele Schleifen spielen bei Program-mierumgebungen wie OpenMP eine wesentliche Rolle.

• Bei der Parallelitat auf Funktionsebene werden ge-samte Funktionsaktivierungen eines Programms parallelzueinander auf verschiedenen Prozessoren oder Prozes-sorkernen ausgefuhrt. Bestehen zwischen parallel aus-zufuhrenden Funktionen Daten- oder Kontrollabhangig-keiten, so ist eine Koordination zwischen den Funktio-nen erforderlich. Dies erfolgt in Form von Kommunika-

Page 40: Multicore Parallele Programmierung Kng617

32 2 Konzepte paralleler Programmierung

tion und Barrier-Anweisungen bei Modellen mit verteil-tem Adressraum. Ein Beispiel ist die Programmierungmit MPI (Message Passing Interface) [59, 61]. Bei Mo-dellen mit gemeinsamem Adressraum ist Synchronisati-on erforderlich; dies ist also fur die Programmierung vonMulticore-Prozessoren notwendig und wird in Kapitel 3vorgestellt.

Explizite oder implizite Parallelitat

Eine Voraussetzung fur die parallele Abarbeitung auf ei-nem Multicore-Prozessor ist das Vorhandensein mehrererBerechnungsstrome. Diese konnen auf recht unterschiedli-che Art erzeugt werden [60].

Bei impliziter Parallelitat braucht der Programmie-rer keine Angaben zur Parallelitat zu machen. Zwei unter-schiedliche Vertreter impliziter Parallelitat sind paralleli-sierende Compiler oder funktionale Programmiersprachen.Parallelisierende Compiler erzeugen aus einem gegebenensequentiellen Programm ein paralleles Programm und nut-zen dabei Abhangigkeitsanalysen, um unabhangige Berech-nungen zu ermitteln. Dies ist in den meisten Fallen einekomplexe Aufgabe und die Erfolge parallelisierender Com-piler sind entsprechend begrenzt [55, 66, 5, 2]. Programmein einer funktionalen Programmiersprache bestehen aus ei-ner Reihe von Funktionsdefinitionen und einem Ausdruck,dessen Auswertung das Programmergebnis liefert. Das Po-tential fur Parallelitat liegt in der parallelen Auswertungder Argumente von Funktionen, da funktionale Program-me keine Seiteneffekte haben und sich die Argumente somitnicht beeinflussen konnen [33, 64, 8]. Implizite Parallelitatwird teilweise auch von neuen Sprachen wie Fortress einge-setzt, vgl. Abschnitt 7.1.

Page 41: Multicore Parallele Programmierung Kng617

2.3 Parallele Programmiermodelle 33

Explizite Parallelitat mit impliziter Zerlegung liegtvor, wenn der Programmierer zwar angibt, wo im Pro-gramm Potential fur eine parallele Bearbeitung vorliegt,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 zusatzlich angeben, welche Tasks es fur die paralleleAbarbeitung geben soll, ohne aber eine Zuordnung an Pro-zesse oder explizite Kommunikation formulieren zu mussen.Ein Beispiel ist BSP [65]. Die explizite Zuordnung der Tasksan Prozesse wird in Koordinationssprachen wie Linda [12]zusatzlich angegeben.

Bei Programmiermodellen mit expliziter Kommuni-kation und Synchronisation muss der Programmiereralle Details der parallelen Abarbeitung angeben. Hierzugehort das Message-Passing-Programmiermodell mit MPI,aber auch Programmierumgebungen zur Benutzung vonThreads, 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 inForm von Tasks werden diese implizit Prozessoren oderKernen zugeordnet. Bei vollkommen explizit paralleler Pro-grammierung sind die Programmierung von Threads odervon 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 konnen. Die Thre-ads eines einzelnen Programmes besitzen fur die Abarbei-tung jeweils eigene Ressourcen, wie Programmzahler, Sta-

Page 42: Multicore Parallele Programmierung Kng617

34 2 Konzepte paralleler Programmierung

tusinformationen des Prozessors oder einen Stack fur loka-le Daten, nutzen jedoch einen gemeinsamen Datenspeicher.Damit ist das Thread-Modell fur die Programmierung vonMulticore-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 undsomit jeweils einen privaten Adressraum besitzen.

Abarbeitung paralleler Programmteile

Die Abarbeitung paralleler Programmteile kann synchronerfolgen, indem die Anweisungen paralleler Threads oderProzesse jeweils gleichzeitig abgearbeitet werden, wie et-wa im SIMD-Modell, oder asynchron, also unabhangig von-einander bis eine explizite Synchronisation erfolgt, wie et-wa im SPMD-Modell. Diese Festlegung der Abarbeitungwird meist vom Programmiermodell der benutzten Pro-grammierumgebung vorgegeben. Daruber hinaus gibt es ei-ne Reihe von Programmiermustern, in denen parallele Pro-grammteile angeordnet werden, z.B. Pipelining, Master-Worker oder Produzenten-Konsumenten-Modell, und dievom Softwareentwickler explizit ausgewahlt werden.

Informationsaustausch

Ein wesentliches Merkmal fur den Informationsaustauschist die Organisation des Adressraums. Bei einem verteiltenAdressraum werden Daten durch Kommunikation ausge-tauscht. Dies kann explizit im Programm angegeben sein,aber auch durch einen Compiler oder das Laufzeitsystemerzeugt werden. Bei einem gemeinsamen Adressraum kann

Page 43: Multicore Parallele Programmierung Kng617

2.4 Parallele Leistungsmaße 35

der Informationsaustausch einfach uber gemeinsame Varia-ble in diesem Adressraum geschehen, auf die lesend oderschreibend zugegriffen werden kann. Hierdurch kann es je-doch auch zu Konflikten oder unerwunschten Ergebnis-sen kommen, wenn dies unkoordiniert erfolgt. Die Koor-dination von parallelen Programmteilen spielt also einewichtige Rolle bei der Programmierung eines gemeinsa-men Adressraums und ist daher ein wesentlicher Bestand-teil der Thread-Programmierung und der Programmierungvon 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 Koordinationvon Threads. Letzteres hat insbesondere mit der Vermei-dung von Konflikten beim Zugriff auf einen gemeinsamenAdressraum zu tun und setzt Sperrmechanismen und be-dingtes Warten ein.

2.4 Parallele Leistungsmaße

Ein wesentliches Kriterium zur Bewertung eines parallelenProgramms ist dessen Laufzeit. Die parallele LaufzeitTp(n) eines Programmes ist die Zeit zwischen dem Startder Abarbeitung des parallelen Programmes und der Been-digung der Abarbeitung aller beteiligten Prozessoren. Dieparallele Laufzeit wird meist in Abhangigkeit von der An-zahl p der zur Ausfuhrung benutzten Prozessoren und ei-ner Problemgroße n angegeben, die z.B. durch die Großeder Eingabe gegeben ist. Fur Multicore-Prozessoren mit ge-meinsamem Adressraum setzt sich die Laufzeit eines paral-lelen Programmes zusammen aus:

Page 44: Multicore Parallele Programmierung Kng617

36 2 Konzepte paralleler Programmierung

• der Zeit fur die Durchfuhrung von Berechnungen durchdie Prozessorkerne,

• der Zeit fur die Synchronisation beim Zugriff auf ge-meinsame Daten,

• den Wartezeiten, die z.B. wegen ungleicher Verteilungder Last oder an Synchronisationspunkten entstehen.

Kosten: Die Kosten eines parallelen Programmes, haufigauch Arbeit oder Prozessor-Zeit-Produkt genannt, be-rucksichtigen die Zeit, die alle an der Ausfuhrung beteilig-ten Prozessoren insgesamt zur Abarbeitung des Program-mes verwenden. Die Kosten Cp(n) eines parallelen Pro-gramms sind definiert als

Cp(n) = Tp(n) · pund sind damit ein Maß fur die von allen Prozessorendurchgefuhrte Arbeit. Ein paralleles Programm heißt kos-tenoptimal, wenn Cp(n) = T ∗(n) gilt, d.h. wenn insge-samt genauso viele Operationen ausgefuhrt werden wie vomschnellsten sequentiellen Verfahren, das Laufzeit T ∗(n) hat.

Speedup: Zur Laufzeitanalyse paralleler Programmeist insbesondere ein Vergleich mit einer sequentiellen Im-plementierung von Interesse, um den Nutzen des Einsatzesder Parallelverarbeitung abschatzen zu konnen. Fur einensolchen Vergleich wird oft der Speedup-Begriff als Maß furden relativen Geschwindigkeitsgewinn herangezogen. DerSpeedup Sp(n) eines parallelen Programmes mit LaufzeitTp(n) ist definiert als

Sp(n) =T ∗(n)Tp(n)

,

wobei p die Anzahl der Prozessoren zur Losung des Pro-blems der Große n bezeichnet. Der Speedup einer paral-lelen Implementierung gibt also den relativen Geschwin-digkeitsvorteil an, der gegenuber der besten sequentiellen

Page 45: Multicore Parallele Programmierung Kng617

2.4 Parallele Leistungsmaße 37

Implementierung durch den Einsatz von Parallelverarbei-tung auf p Prozessoren entsteht. Theoretisch gilt immerSp(n) ≤ p. Durch Cacheeffekte kann in der Praxis auchder Fall Sp(n) > p (superlinearer Speedup) auftreten.

Effizienz: Alternativ zum Speedup kann der Begriff derEffizienz eines parallelen Programmes benutzt werden, derein Maß fur den Anteil der Laufzeit ist, den ein Prozessorfur Berechnungen benotigt, die auch im sequentiellen Pro-gramm vorhanden sind. Die Effizienz Ep(n) eines parallelenProgramms ist definiert als

Ep(n) =T ∗(n)Cp(n)

=Sp(n)

p=

T ∗(n)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 SpeedupSp(n) = p entspricht einer Effizienz Ep(n) = 1.

Amdahlsches Gesetz: Die mogliche Verringerung vonLaufzeiten durch eine Parallelisierung sind oft begrenzt. Sostellt etwa die Anzahl der Prozessoren die theoretisch obe-re Schranke des Speedups dar. Weitere Begrenzungen lie-gen im zu parallelisierenden Algorithmus selber begrundet,der neben parallelisierbaren Anteilen auch durch Daten-abhangigkeiten bedingte, inharent sequentielle Anteile ent-halten kann. Der Effekt von Programmteilen, die sequentiellausgefuhrt werden mussen, auf den erreichbaren Speedupwird durch das Amdahlsche Gesetz quantitativ erfasst[6]:

Wenn bei einer parallelen Implementierung ein (kon-stanter) Bruchteil f (0 ≤ f ≤ 1) sequentiell ausgefuhrtwerden muss, setzt sich die Laufzeit der parallelen Imple-mentierung aus der Laufzeit f · T ∗(n) des sequentiellenTeils und der Laufzeit des parallelen Teils, die mindestens(1− f)/p · T ∗(n) betragt, zusammen. Fur den erreichbaren

Page 46: Multicore Parallele Programmierung Kng617

38 2 Konzepte paralleler Programmierung

Speedup gilt damit

Sp(n) =T ∗(n)

f · T ∗(n) + 1−fp T ∗(n)

=1

f + 1−fp

≤ 1f

.

Bei dieser Berechnung wird der beste sequentielle Al-gorithmus verwendet und es wurde angenommen, dass sichder parallel ausfuhrbare Teil perfekt parallelisieren lasst.Durch ein einfaches Beispiel sieht man, dass nicht paral-lelisierbare Berechnungsteile einen großen Einfluss auf denerreichbaren Speedup haben: Wenn 20% eines Programmessequentiell abgearbeitet werden mussen, betragt nach Aus-sage des Amdahlschen Gesetzes der maximal erreichbareSpeedup 5, egal wie viele Prozessoren eingesetzt werden.Nicht parallelisierbare Teile mussen insbesondere bei einergroßen Anzahl von Prozessoren besonders beachtet werden.

Skalierbarkeit: Das Verhalten der Leistung eines par-allelen Programmes bei steigender Prozessoranzahl wirddurch die Skalierbarkeit erfasst. Die Skalierbarkeit einesparallelen Programmes auf einem gegebenen Parallelrech-ner ist ein Maß fur die Eigenschaft, einen Leistungsgewinnproportional zur Anzahl p der verwendeten Prozessorenzu erreichen. Der Begriff der Skalierbarkeit wird in unter-schiedlicher Weise prazisiert, z.B. durch Einbeziehung derProblemgroße n. Eine haufig beobachtete Eigenschaft par-alleler Algorithmen ist es, dass fur festes n und steigendesp eine Sattigung des Speedups eintritt, dass aber fur fes-tes p und steigende Problemgroße n ein hoherer Speeduperzielt wird. In diesem Sinne bedeutet Skalierbarkeit, dassdie Effizienz eines parallelen Programmes bei gleichzeiti-gem Ansteigen von Prozessoranzahl p und Problemgroße nkonstant bleibt.

Page 47: Multicore Parallele Programmierung Kng617

3

Thread-Programmierung

Die Programmierung von Multicore-Prozessoren ist eng mitder parallelen Programmierung eines gemeinsamen Adress-raumes und der Thread-Programmierung verbunden. Meh-rere Berechnungsstrome desselben Programms konnen par-allel zueinander bearbeitet werden und greifen dabei aufVariablen des gemeinsamen Speichers zu. Diese Berech-nungsstrome werden als Threads bezeichnet. Die Pro-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 oderWin32 fur Multithreading-Programme genutzt werden.

3.1 Threads und Prozesse

Die Abarbeitung von Threads hangt eng mit der Abar-beitung von Prozessen zusammen, so dass beide zunachstnochmal genauer definiert und voneinander abgegrenzt wer-den.

Page 48: Multicore Parallele Programmierung Kng617

40 3 Thread-Programmierung

Prozesse

Ein Prozess ist ein sich in Ausfuhrung befindendes Pro-gramm und umfasst neben dem ausfuhrbaren Programmco-de alle Informationen, die zur Ausfuhrung des Programmserforderlich sind. Dazu gehoren die Daten des Programmsauf dem Laufzeitstack oder Heap, die zum Ausfuhrungszeit-punkt aktuellen Registerinhalte und der aktuelle Wert desProgrammzahlers, der die nachste auszufuhrende Instruk-tion des Prozesses angibt. Jeder Prozess hat also seineneigenen Adressraum. Alle diese Informationen andern sichwahrend der Ausfuhrung des Prozesses dynamisch. Wirddie Rechenressource einem anderen Prozess zugeordnet, somuss der Zustand des suspendierten Prozesses gespeichertwerden, damit die Ausfuhrung dieses Prozesses zu einemspateren Zeitpunkt mit genau diesem Zustand fortgesetztwerden kann. Dies wird als Kontextwechsel bezeichnetund ist je nach Hardwareunterstutzung relativ aufwendig[54]. Prozesse werden bei Multitasking im Zeitscheibenver-fahren von der Rechenressource abgearbeitet; es handeltsich also um Nebenlaufigkeit und keine Gleichzeitigkeit. BeiMultiprozessor-Systemen ist eine tatsachliche Parallelitatmoglich.

Beim Erzeugen eines Prozesses muss dieser die zu sei-ner Ausfuhrung erforderlichen Daten erhalten. Im UNIX-Betriebssystem kann ein Prozess P1 mit Hilfe einer fork-Anweisung einen neuen Prozess P2 erzeugen. Der neueKindprozess 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 Programmwie der Elternprozess ausfuhrt, und zwar ab der der fork-Anweisung folgenden Anweisung. Der Kindprozess hat je-doch eine eigene Prozessnummer und kann in Abhangigkeit

Page 49: Multicore Parallele Programmierung Kng617

3.1 Threads und Prozesse 41

von dieser Prozessnummer andere Anweisungen als der El-ternprozess ausfuhren, vgl. [46]. Da jeder Prozess einen ei-genen Adressraum hat, ist die Erzeugung und Verwaltungvon Prozessen je nach Große des Adressraumes relativ zeit-aufwendig. Weiter kann bei haufiger Kommunikation derAustausch von Daten (uber Sockets) einen nicht unerheb-lichen Anteil der Ausfuhrungszeit ausmachen.

Threads

Das Threadmodell ist eine Erweiterung des Prozessmodells.Jeder Prozess besteht anstatt nur aus einem aus mehrerenunabhangigen Berechnungsstromen, die wahrend der Ab-arbeitung des Prozesses durch ein Schedulingverfahren derRechenressource zugeteilt werden. Die Berechnungsstromeeines Prozesses werden als Threads bezeichnet. Das WortThread wurde gewahlt, um anzudeuten, dass eine zusam-menhangende, evtl. sehr lange Folge von Instruktionen ab-gearbeitet wird.

Ein wesentliches Merkmal von Threads besteht dar-in, dass die verschiedenen Threads eines Prozesses sichden Adressraum des Prozesses teilen, also einen gemein-samen Adressraum haben. Wenn ein Thread einen Wert imAdressraum ablegt, kann daher ein anderer Thread des glei-chen Prozesses diesen unmittelbar darauf lesen. Damit istder Informationsaustausch zwischen Threads im Vergleichzur Kommunikation zwischen Prozessen uber Sockets sehrschnell. 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. DasKopieren des Adressraumes, das z.B. in UNIX beim Er-zeugen von Prozessen mit einer fork-Anweisung notwendigist, entfallt. Das Arbeiten mit mehreren Threads innerhalbeines Prozesses ist somit wesentlich flexibler als das Arbei-

Page 50: Multicore Parallele Programmierung Kng617

42 3 Thread-Programmierung

ten mit kooperierenden Prozessen, bietet aber die gleichenVorteile. Insbesondere ist es moglich, die Threads eines Pro-zesses auf verschiedenen Prozessoren oder Prozessorkernenparallel auszufuhren.

Threads konnen auf Benutzerebene als Benutzer-Thre-ads oder auf Betriebssystemebene als Betriebssystem-Threads implementiert werden. Threads auf Benutzerebe-ne werden durch eine Thread-Bibliothek ohne Beteiligungdes Betriebssystems verwaltet. Ein Wechsel des ausgefuhr-ten Threads kann damit ohne Beteiligung des Betriebssys-tems erfolgen und ist daher in der Regel wesentlich schnellerals der Wechsel bei Betriebssystem-Threads.

Betriebssystem−

Betriebssystem−

T

T

T

P

P

P

P

BP

BP

BP

BP

BP

BP

BPScheduler

Bibliotheks−

Bibliotheks−Scheduler

Prozesse

T

T

T

T

Prozess 1

Scheduler

Prozessoren

Prozess n

Abbildung 3.1. N:1-Abbildung – Thread-Verwaltung oh-ne Betriebssystem-Threads. Der Scheduler der Thread-Bibliothekwahlt den auszufuhrenden Thread T des Benutzerprozesses aus.Jedem Benutzerprozess ist ein Betriebssystemprozss BP zugeord-net. Der Betriebssystem-Scheduler wahlt die zu einem bestimmtenZeitpunkt auszufuhrenden Betriebssystemprozesse aus und bildetdiese auf die Prozessoren P ab.

Page 51: Multicore Parallele Programmierung Kng617

3.1 Threads und Prozesse 43

Der Nachteil von Threads auf Benutzerebene liegt darin,dass das Betriebssystem keine Kenntnis von den Threadshat und nur gesamte Prozesse verwaltet. Wenn ein Threadeines Prozesses das Betriebssystem aufruft, um z.B. eineI/O-Operation durchzufuhren, wird der CPU-Scheduler desBetriebssystems den gesamten Prozess suspendieren unddie Rechenressource einem anderen Prozess zuteilen, da dasBetriebssystem nicht weiß, dass innerhalb des Prozesses zueinem anderen Thread umgeschaltet werden kann. Dies giltfur Betriebssystem-Threads nicht, da das Betriebssystemdie Threads direkt verwaltet.

Prozess n

Prozess 1

Betriebssystem−

Betriebssystem−

T

T

T

T

T

T

T

P

P

P

P

BT

BT

BT

BT

BT

BT

BT

Threads

Prozessoren

Scheduler

Abbildung 3.2. 1:1-Abbildung – Thread-Verwaltung mitBetriebssystem-Threads. Jeder Benutzer-Thread T wird eindeutigeinem Betriebssystem-Thread BT zugeordnet.

Page 52: Multicore Parallele Programmierung Kng617

44 3 Thread-Programmierung

Betriebssystem−

T

T

T

T

T

T

T

P

P

P

P

BT

BT

BT

BT

BT

BT

BT

Prozess n

Scheduler

Bibliotheks−Scheduler

Betriebssystem−Threads

Prozessoren

Scheduler

Prozess 1

Bibliotheks−

Abbildung 3.3. N:M-Abbildung – Thread-Verwaltung mitBetriebssystem-Threads und zweistufigem Scheduling. Benutzer-Threads T verschiedener Prozesse werden einer Menge vonBetriebssystem-Threads BT zugeordnet (N:M-Abbildung).

Ausfuhrungsmodelle fur Threads

Wird eine Thread-Verwaltung durch das Betriebssystemnicht unterstutzt, so ist die Thread-Bibliothek fur das Sche-duling der Threads verantwortlich. Alle Benutzer-Threadseines Prozesses werden vom Bibliotheks-Scheduler auf einenBetriebssystem-Prozess abgebildet, was N:1-Abbildunggenannt wird, siehe Abb. 3.1. Stellt das Betriebssystem eineThread-Verwaltung zur Verfugung, so gibt es fur die Ab-bildung von Benutzer-Threads auf Betriebssystem-Threadszwei Moglichkeiten: Die erste ist die 1:1-Abbildung, diefur jeden Benutzer-Thread einen Betriebssystem-Threaderzeugt, siehe Abb. 3.2. Der Betriebssystem-Scheduler wahltden jeweils auszufuhrenden Betriebssystem-Thread aus undverwaltet bei Mehr-Prozessor-Systemen die Ausfuhrung derBetriebssystem-Threads auf den verschiedenen Prozesso-

Page 53: Multicore Parallele Programmierung Kng617

3.1 Threads und Prozesse 45

ren. Die zweite Moglichkeit ist die N:M-Abbildung, dieein 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, wobeiein Benutzer-Thread zu verschiedenen Zeitpunkten auf ver-schiedene Betriebssystem-Threads abgebildet werden kann.

Zustande eines Threads

Ob ein Thread gerade von einem Prozessor oder Prozes-sorkern abgearbeitet wird, hangt nicht nur vom Scheduler,sondern auch von seinem Zustand ab. Threads konnen sichin verschiedenen Zustanden befinden:

• neu erzeugt• lauffahig• laufend• wartend• beendet

wartend

lauf−fähig

beendetneu

laufend

EndeStart

Zuteilung

Unterbrechung

Aufwecken Blockierung

Abbildung 3.4. Zustande eines Thre-ads.

Abbildung 3.4 ver-anschaulicht die Zu-standsubergange. DieUbergange zwischenlauffahig und laufendwerden vom Schedu-ler bestimmt. Blockierung bzw. Warten kann durch I/O-Operationen, aber auch durch die Koordination zwischenden Threads eintreten.

Sichtbarkeit von Daten

Die Threads eines Prozesses teilen sich einen gemeinsamenAdressraum, d.h. die globalen Variablen und alle dynamisch

Page 54: Multicore Parallele Programmierung Kng617

46 3 Thread-Programmierung

Stackbereichfür Hauptthread

Stackbereichfür Thread 1

Stackbereichfür Thread 2

Heapdaten

globale Daten

...

Adresse 0

}}}

Stackdaten

Stackdaten

Programmcode

Stackdaten

Abbildung 3.5. Laufzeitverwaltung fur ein Programm mit meh-reren Threads.

erzeugten Datenobjekte sind von allen erzeugten Threadsdes Prozesses zugreifbar. Fur jeden Thread wird jedoch eineigener Laufzeitstack gehalten, auf dem die von dem Threadaufgerufenen Funktionen mit ihren lokalen Variablen ver-waltet werden, siehe Abb. 3.5. Die auf dem Laufzeitstackverwalteten, also statisch deklarierten Daten, sind lokaleDaten des zugehorigen Threads und konnen von anderenThreads nicht zugegriffen werden. Da der Laufzeitstack ei-nes Threads nur so lange existiert, wie der Thread selbst,kann ein Thread einen moglichen Ruckgabewert an einenanderen Thread nicht uber seinen Laufzeitstack ubergebenund es mussen andere Techniken verwendet werden.

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-

Page 55: Multicore Parallele Programmierung Kng617

3.2 Synchronisations-Mechanismen 47

ler eingesetzt, um eine gewunschte Ausfuhrungsreihenfolgeder beteiligten Threads zu erreichen oder um den Zugriffauf gemeinsame Daten zu gestalten. Die Koordination desZugriffs auf den gemeinsamen Speicher dient der Vermei-dung von unerwunschten Effekten beim gleichzeitigen Zu-griff auf dieselbe Variable. Dies trifft fur Multithreading-Programme auf einer Rechenressource mit nebenlaufigerAbarbeitung im Zeitscheibenverfahren zu, aber auch aufeine tatsachlich parallele Abarbeitung auf mehreren Re-chenressourcen. Da die Threads eines Prozesses im We-sentlichen uber gemeinsame Daten kooperieren, bewirkt ei-ne bestimmte Ausfuhrungsreihenfolge einen speziellen Zu-stand des gemeinsamen Speichers, also eine bestimmte Be-legung der gemeinsamen Variablen mit Werten, die fur alleThreads sichtbar sind. Die Effekte der Kooperation und Ko-ordination konnen jedoch bei Nebenlaufigkeit anders seinals bei Parallelitat.

Koordination der Berechnungsstrome

Eine Barrier-Synchronisation bewirkt, dass alle betei-ligten Threads aufeinander warten und keiner der Threadseine nach der Synchronisationsanweisung stehende Anwei-sung ausfuhrt, bevor alle anderen Threads diese erreicht ha-ben. Dadurch erscheint den Threads der gemeinsame Spei-cher aller beteiligten Threads in dem Zustand, der durchdie Abarbeitung aller Anweisungen aller Threads vor derBarrier-Synchronisation erreicht wird.

Zeitkritische Ablaufe

Das lesende und schreibende Zugreifen verschiedener Thre-ads auf dieselbe gemeinsame Variable kann zu sogenann-ten zeitkritischen Ablaufen fuhren. Dies bedeutet, dass

Page 56: Multicore Parallele Programmierung Kng617

48 3 Thread-Programmierung

das Ergebnis der Ausfuhrung eines Programmstucks durchmehrere Threads von der relativen Ausfuhrungsgeschwin-digkeit der Threads zueinander abhangt: Wenn das Pro-grammstuck zuerst von Thread T1 und dann von ThreadT2 ausgefuhrt wird, kann ein anderes Ergebnis berechnetwerden als wenn dieses Programmstuck zuerst von ThreadT2 und dann von Thread T1 ausgefuhrt wird. Das Auf-treten von zeitkritischen Ablaufen ist meist unerwunscht,da die relative Ausfuhrungsreihenfolge von vielen Fakto-ren abhangen kann (z.B. der Ausfuhrungsgeschwindigkeitder Prozessoren, dem Auftreten von Interrupts oder derBelegung der Eingabedaten), die vom Programmierer nurbedingt zu beeinflussen sind. Es entsteht ein nichtdeter-ministisches Verhalten, da fur die Ausfuhrungsreihenfol-ge und das Ergebnis verschiedene Moglichkeiten eintretenkonnen, ohne dass dies vorhergesagt werden kann.

Kritischer Bereich

Ein Programmstuck, in dem Zugriffe auf gemeinsame Varia-blen vorkommen, die auch konkurrierend von anderen Thre-ads zugegriffen werden konnen, heißt kritischer Bereich.Eine fehlerfreie Abarbeitung kann dadurch gewahrleistetwerden, dass durch einen wechselseitigen Ausschluss(oder mutual exclusion) jeweils nur ein Thread auf Varia-blen zugreifen kann, die in einem kritischen Bereich liegen.Programmiermodelle fur einen gemeinsamen Adressraumstellen Operationen und Mechanismen zur Sicherstellungdes wechselseitigen Ausschlusses zur Verfugung, mit de-nen erreicht werden kann, dass zu jedem Zeitpunkt nur einThread auf eine gemeinsame Variable zugreift. Die grund-legenden Mechanismen, die angeboten werden, sind Sperr-mechanismus und Bedingungs-Synchronisation.

Page 57: Multicore Parallele Programmierung Kng617

3.2 Synchronisations-Mechanismen 49

Sperrmechanismus

Zur Vermeidung des Auftretens zeitkritischer Ablaufe mitHilfe eines Sperrmechanismus wird eine Sperrvariable(oder Mutex-Variable von mutual exclusion) s eines spezi-ell vorgegebenen Typs verwendet, die mit den Funktionenlock(s) und unlock(s) angesprochen wird. Vor Betreten deskritischen Bereichs fuhrt der Thread lock(s) zur Belegungder Sperrvariable s aus; nach Verlassen des Programmseg-ments wird unlock(s) zur Freigabe der Sperrvariable aufge-rufen. Nur wenn jeder Prozessor diese Vereinbarung einhalt,werden zeitkritische Ablaufe vermieden.

Der Aufruf lock(s) hat den Effekt, dass der aufrufendeThread T1 nur dann das dieser Sperrvariablen zugeordneteProgrammsegment ausfuhren kann, wenn gerade kein an-derer Thread diese Sperrvariable belegt hat. Hat jedoch einanderer Thread T2 zuvor lock(s) ausgefuhrt und die Sperr-variable s noch nicht mit unlock(s) wieder freigegeben, sowird 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 Verwendungeines Sperrmechanismus kann also zu einer Sequentiali-sierung fuhren, da Threads durch ihn nur nacheinanderauf eine gemeinsame Variable zugreifen konnen. Sperrme-chanismen sind in Laufzeitbibliotheken wie Pthreads, Java-Threads oder OpenMP auf leicht unterschiedliche Art rea-lisiert.

Bedingungs-Synchronisation

Bei einer Bedingungs-Synchronisation wird ein ThreadT1 so lange blockiert bis eine bestimmte Bedingung ein-getreten ist. Das Aufwecken des blockierten Threads kann

Page 58: Multicore Parallele Programmierung Kng617

50 3 Thread-Programmierung

nur durch einen anderen Thread T2 erfolgen. Dies geschiehtsinnvollerweise nachdem durch die Ausfuhrung von ThreadT2 diese Bedingung eingetreten ist. Da jedoch mehrereThreads auf dem gemeinsamen Adressraum arbeiten unddadurch zwischenzeitlich wieder Veranderungen der Bedin-gung erfolgt sein konnten, muss die Bedingung durch denaufgeweckten Thread T1 nochmal uberpruft werden. DieBedingungs-Synchronisation wird durch Bedingungsva-riablen realisiert; zum Schutz vor zeitkritischen Ablaufenwird zusatzlich ein Sperrmechanismus verwendet.

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 enthalt, aufdie zwei atomare Operationen P (s) und V (s) angewendetwerden konnen. Ein binarer Semaphor kann nur die Werte0 und 1 annehmen. Werden weitere Werte angenommen,spricht man von einem zahlenden Semaphor. Die Operati-on P (s) (oder wait(s)) wartet bis der Wert von s großerals 0 ist, dekrementiert den Wert von s anschließend um 1und erlaubt dann die weitere Ausfuhrung der nachfolgendenBerechnungen. Die Operation V (s) (oder signal(s)) inkre-mentiert den Wert von s um 1. Der genaue Mechanismusder Verwendung von P und V zum Schutz eines kritischenBereiches ist nicht streng festgelegt. Eine ubliche Form ist:

wait(s)kritischer Bereichsignal(s)

Verschiedene Threads fuhren die Operationen P und V aufs aus und koordinieren so ihren Zugriff auf kritische Berei-

Page 59: Multicore Parallele Programmierung Kng617

3.3 Effiziente und korrekte Thread-Programme 51

che. Fuhrt etwa Threads T1 die Operation wait(s) aus umdanach seinen kritischen Bereich zu bearbeiten, so wird je-der andere Threads T2 beim Aufruf von wait(s) am Eintrittin seinen kritischen Bereich so lange gehindert, bis T1 dieOperation signal(s) ausfuhrt.

Monitor

Ein abstrakteres Konzept stellt der Monitor dar [31]. EinMonitor 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 durchdessen Monitoroperationen zugegriffen werden. Da zu je-dem Zeitpunkt die Ausfuhrung nur einer Monitoroperationerlaubt ist, wird der wechselseitige Ausschluss bzgl. der Da-ten des Monitors automatisch sichergestellt.

3.3 Effiziente und korrekteThread-Programme

Je nach Applikation kann durch Synchronisation eine engeund komplizierte Verzahnung von Threads entstehen, waszu Problemen wie Leistungseinbußen durch Sequentialisie-rung oder sogar zu Deadlocks fuhren kann.

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 beimEntwurf darauf geachtet werden, dass

• eine geeignete Anzahl von Threads genutzt wird und

Page 60: Multicore Parallele Programmierung Kng617

52 3 Thread-Programmierung

• Sequentialisierungen nach Moglichkeit vermieden wer-den.

Die Erzeugung von Threads bewirkt Parallelitat, so dasseine hinreichend große Anzahl von Threads im parallelenProgramm vorhanden sein sollte, um alle Prozessorkernemit Arbeit zu versorgen und so die verfugbaren parallelenRessourcen gut auszunutzen. Andererseits sollte die Anzahlder Threads auch nicht zu groß werden, da erstens der An-teil der Arbeit fur einen einzelnen Thread im Vergleich zumOverhead fur Erzeugung, Verwaltung und Terminierung desThreads zu gering werden kann, und da zweitens viele Hard-wareressourcen (vor allem Caches) von den Prozessorkernengeteilt werden und es so zu Leistungsverlusten bei der Lese-/Schreib-Bandbreite kommen kann.

Aufgrund der notwendigen Kooperationen zwischen denThreads kann die vorgegebene Parallelitat nicht immerausgenutzt werden, da zur Vermeidung von zeitkritischenAblaufen Synchronisations-Mechanismen eingesetzt werdenmussen. Bei haufiger Synchronisation kann es jedoch dazukommen, dass immer nur einer oder wenige der Threadsaktiv sind, wahrend alle anderen auf Grund der Synchroni-sation warten, so dass eine Nacheinanderausfuhrung, alsoSequentialisierung, auftritt.

Deadlock

Die Nutzung von Sperr- und anderen Synchronisations-Mechanismen hilft Nichtdeterminismus und zeitkritischeAblaufe zu vermeiden. Die Nutzung von Sperren kann je-doch zu einem Deadlock (Verklemmung) im Anwendungs-programm fuhren, wenn die Abarbeitung in einen Zustandkommt, in dem jeder Thread auf ein Ereignis wartet, dasnur von einem anderen Thread ausgelost werden kann, deraber auch vergeblich auf ein Ereignis wartet.

Page 61: Multicore Parallele Programmierung Kng617

3.3 Effiziente und korrekte Thread-Programme 53

Allgemein ist ein Deadlock fur eine Menge von Akti-vitaten dann gegeben, wenn jede der Aktivitaten auf einEreignis wartet, das nur durch eine der anderen Aktivitatenhervorgerufen werden kann, so dass ein Zyklus des gegen-seitigen Aufeinanderwartens entsteht. Ein Beispiel fur einenDeadlock 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, wartenbeide Threads auf die Freigabe der fehlenden Sperre durchden jeweils anderen Thread, die nicht eintreten kann.

Die Verwendung von Sperrmechanismen sollte also gutgeplant sein, um diesen Fall etwa durch eine spezielle Rei-henfolge der Sperrbelegung zu vermeiden, vgl. auch [59].

Speicherzugriffszeiten und Cacheeffekte

Speicherzugriffszeiten konnen einen hohen Anteil an derparallelen Laufzeit eines Programms haben. Die Speicher-zugriffe eines Programms fuhren zum Transfer von Datenzwischen Speicher und den Caches der Prozessorkerne. Die-ser Datentransfer wird durch Lese- und Schreiboperationender Kerne ausgelost und kann nicht direkt vom Program-mierer gesteuert werden.

Zwischen Datenzugriffen verschiedener Prozessorkernekonnen verschiedene Abhangigkeiten auftreten: Lese-Lese-Abhangigkeiten, Lese-Schreib-Abhangigkeiten und Schreib-Schreib-Abhangigkeiten. Lesen zwei Prozessorkerne diesel-ben Daten, so kann dies evtl. ohne Speicherzugriff aus denjeweiligen Caches erfolgen. Die beiden anderen Abhangig-keiten losen Speicherzugriffe aus, da die Daten zwischen

Page 62: Multicore Parallele Programmierung Kng617

54 3 Thread-Programmierung

den Prozessorkernen ausgetauscht werden mussen. Die An-zahl der Speicherzugriffe kann reduziert werden, indem derZugriff der Prozessorkerne auf gemeinsame Daten so gestal-tet wird, dass die Kerne auf verschiedene Daten zugreifen.Dies sollte bereits beim Entwurf des parallelen Programmsberucksichtigt werden.

False Sharing, bei dem zwei verschiedene Threads aufverschiedene Daten zugreifen, die jedoch in derselben Ca-chezeile liegen, lost jedoch ebenfalls Speicheroperationenaus. False Sharing kann vom Programmierer nur schwerbeeinflusst werden, da auch eine weit auseinandergezogeneAbspeicherung von Daten nicht immer zum Erfolg fuhrt.

3.4 Parallele Programmiermuster

Parallele oder verteilte Programme bestehen aus einer An-sammlung von Tasks, die in Form von Threads auf verschie-denen Rechenressourcen ausgefuhrt werden. Zur Struktu-rierung der Programme konnen parallele Muster verwendetwerden, die sich in der parallelen Programmierung als sinn-voll herausgestellt haben, siehe z.B. [56] oder [45]. DieseMuster geben eine spezielle Koordinationsstruktur der be-teiligten Threads vor.

Erzeugung von Threads

Die Erzeugung von Threads kann statisch oder dynamischerfolgen. Im statischen Fall wird meist eine feste Anzahlvon Threads zu Beginn der Abarbeitung des parallelen Pro-gramms erzeugt, die wahrend der gesamten Abarbeitungexistieren und erst am Ende des Gesamtprogramms beendetwerden. Alternativ konnen Threads zu jedem Zeitpunkt derProgrammabarbeitung (statisch oder dynamisch) erzeugt

Page 63: Multicore Parallele Programmierung Kng617

3.4 Parallele Programmiermuster 55

und beendet werden. Zu Beginn der Abarbeitung ist meistnur 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], dasvon der Programmierung mit Prozessen herruhrt, aber alsMuster auch fur Threads anwendbar ist. Ein bereits existie-render Thread T1 spaltet mit einem Fork-Aufruf einen wei-teren Thread T2 ab. Bei einem zugeordneten Join-Aufrufdes Threads T1 wartet dieser auf die Beendigung des Thre-ads T2.

Das Fork-Join-Konzept kann explizit als Sprachkon-strukt oder als Bibliotheksaufruf zur Verfugung stehenund wird meist in der Programmierung mit gemeinsamemAdressraum verwendet. Die Spawn- und Exit-Operationender 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, erlaubtes durch verschachtelte Aufrufe eine beliebige Strukturparalleler Aktivitat. Spezielle Programmiersprachen undProgrammierumgebungen haben oft eine spezifische Aus-pragung der beschriebenen Erzeugung von Threads.

Parbegin-Parend

Eine strukturierte Variante der Thread-Erzeugung wirddurch das gleichzeitige Erzeugen und Beenden mehrererThreads erreicht. Dazu wird das Parbegin-Parend-Kon-strukt bereitgestellt, das manchmal auch mit dem NamenCobegin-Coend bezeichnet wird. Zwischen Parbegin und

Page 64: Multicore Parallele Programmierung Kng617

56 3 Thread-Programmierung

Parend werden Anweisungen angegeben, die auch Funkti-onsaufrufe beinhalten konnen und die Threads zur Ausfuh-rung zugeordnet werden konnen. Erreicht der ausfuhrendeThread den Parbegin-Befehl, so werden die von Parbegin-Parend umgebenen Anweisungen separaten Threads zurAusfuhrung zugeordnet. Der Programmtext nach dem Par-end-Befehl wird erst ausgefuhrt, wenn alle so gestartetenThreads beendet sind. Die Threads innerhalb des Parbegin-Parend-Konstrukts konnen gleichen oder verschiedenen Pro-grammtext haben. Ob und wie die Threads tatsachlich par-allel ausgefuhrt werden, hangt von der zur Verfugung ste-henden Hardware und der Implementierung des Konstruktsab. Die Anzahl und Art der zu erzeugenden Threads stehtmeist statisch fest. Auch fur dieses Konstrukt haben spe-zielle parallele Sprachen oder Umgebungen ihre spezifischeSyntax und Auspragung, wie z.B. in Form von parallelenBereichen (parallel sections), vgl. auch OpenMP.

SPMD und SIMD

Im SIMD- (Single Instruction, Multiple Data) und SPMD-Programmiermodell (Single Program, Multiple Data) wirdzu Programmbeginn eine feste Anzahl von Threads gestar-tet. Alle Threads fuhren dasselbe Programm aus, das sie aufverschiedene Daten anwenden. Durch Kontrollanweisungeninnerhalb des Programmtextes kann jeder Thread verschie-dene Programmteile auswahlen und ausfuhren. Im SIMD-Ansatz werden die einzelnen Instruktionen synchron abge-arbeitet, d.h. die verschiedenen Threads arbeiten dieselbeInstruktion gleichzeitig ab. Der Ansatz wird auch haufig alsDatenparallelitat im engeren Sinne bezeichnet. Im SPMD-Ansatz konnen die Threads asynchron arbeiten, d.h. zueinem Zeitpunkt konnen verschiedene Threads verschie-dene Programmstellen bearbeiten. Dieser Effekt tritt ent-

Page 65: Multicore Parallele Programmierung Kng617

3.4 Parallele Programmiermuster 57

weder durch unterschiedliche Ausfuhrungsgeschwindigkei-ten oder eine Verzogerung des Kontrollflusses in Abhangig-keit von lokalen Daten auf. Der SPMD-Ansatz ist z.Zt. ei-ner der popularsten Ansatze der parallelen Programmie-rung, insbesondere in der Programmierung mit verteiltemAdressraum mit MPI. Besonders geeignet ist die SPMD-Programmierung fur Anwendungsalgorithmen, die auf Fel-dern arbeiten und bei denen eine Zerlegung der Felder dieGrundlage einer Parallelisierung ist.

Master-Slave oder Master-Worker

Bei diesem Ansatz kontrolliert ein einzelner Thread diegesamte Arbeit eines Programms. Dieser Master-Threadentspricht oft dem Hauptprogramm des Anwendungspro-gramms. Der Master-Prozess erzeugt mehrere, meist gleich-artige Worker- oder Slave-Threads, die die eigentlichenBerechnungen ausfuhren, siehe Abb. 3.6. Diese Worker-Threads konnen statisch oder dynamisch erzeugt werden.Die Zuteilung von Arbeit an die Worker-Threads kanndurch den Master-Thread erfolgen. Die Worker-Threadskonnen aber auch eigenstandig Arbeit allokieren. In diesemFall ist der Master-Thread nur fur alle ubrigen Koordina-tionsaufgaben zustandig, wie etwa Initialisierung, Zeitmes-sung oder Ausgabe.

Client-Server-Modell

Programmierstrukturierungen nach dem Client-Server-Prin-zip ahneln dem MPMD-Modell (Multiple Program, Mul-tiple Data). Es stammt aus dem verteilten Rechnen, womehrere Client-Rechner mit einem als Server dienendenMainframe verbunden sind, der etwa Anfragen an eineDatenbank bedient. Parallelitat kann auf der Server-Seite

Page 66: Multicore Parallele Programmierung Kng617

58 3 Thread-Programmierung

Anfra

ge

ServerMaster

Slave 1 Slave 3

SteuerungSteu

erun

g

Slave 2

Ste

uer

un

gClient 1

Client 2

Client 3An

frag

e

An

two

rt

Anfrage

Antw

ort

Antwort

Abbildung 3.6. Veranschaulichung Master-Slave-Modell (links)und Client-Server-Modell (rechts).

auftreten, indem mehrere Client-Anfragen verschiedenerClients nebenlaufig oder parallel zueinander beantwortetwerden. Eine parallele Programmstrukturierung nach demClient-Server-Prinzip nutzt mehrere Client-Threads, die An-fragen an einen Server-Thread stellen, siehe Abb. 3.6. NachErledigung der Anfrage durch den Server-Thread geht dieAntwort an den jeweiligen Client-Thread zuruck. Das Client-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 Servernubernehmen und sowohl Anfragen stellen als auch beant-worten konnen.

Pipelining

Der Pipelining-Ansatz beschreibt eine spezielle Form derZusammenarbeit verschiedener Threads, bei der Daten zwi-schen den Threads weitergereicht werden. Die beteiligtenThreads T1, . . . , Tp sind dazu logisch in einer vorgegebe-nen Reihenfolge angeordnet. Thread Ti erhalt die Ausgabevon Thread Ti−1 als Eingabe und produziert eine Ausgabe,die dem nachsten Thread Ti+1, i = 2, . . . , p − 1 als Einga-be dient; Thread T1 erhalt die Eingabe von anderen Pro-grammteilen und Tp gibt seine Ausgabe an wiederum ande-

Page 67: Multicore Parallele Programmierung Kng617

3.4 Parallele Programmiermuster 59

re Progammteile weiter. Jeder Thread verarbeitet also einenStrom von Eingaben in einer sequentiellen Reihenfolge undproduziert einen Strom von Ausgaben. Somit konnen dieThreads durch Anwendung des Pipeline-Prinzips trotz derDatenabhangigkeiten parallel zueinander ausgefuhrt wer-den.

Pipelining kann als spezielle Form einer funktionalenZerlegung betrachtet werden, bei der die Threads Funktio-nen eines Anwendungsalgorithmus bearbeiten, die durch ih-re Datenabhangigkeiten nicht nacheinander ausgefuhrt wer-den, sondern auf die beschriebene Weise gleichzeitig abgear-beitet werden konnen. Das Pipelining-Konzept kann prin-zipiell mit gemeinsamem Adressraum oder mit verteiltemAdressraum realisiert werden.

Taskpools

Ein Taskpool ist eine Datenstruktur, in der die noch abzu-arbeitenden Programmteile (Tasks) eines Programms etwain Form von Funktionen abgelegt sind. Fur die Abarbeitungder Tasks wird eine feste Anzahl von Threads verwendet,die zu Beginn des Programms vom Haupt-Thread erzeugtwerden und bis zum Ende des Programms existieren. Furdie Threads ist der Taskpool eine gemeinsame Datenstruk-tur, auf die sie zugreifen konnen, um die dort abgelegtenTasks zu entnehmen und anschließend abzuarbeiten, sie-he Abb. 3.7. Wahrend der Abarbeitung einer Task kannein Thread neue Tasks erzeugen und diese in den Taskpooleinfugen. Der Zugriff auf den Taskpool muss synchronisiertwerden. Die Abarbeitung des parallelen Programms ist be-endet, wenn der Taskpool leer ist und jeder Thread seineTasks 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

Page 68: Multicore Parallele Programmierung Kng617

60 3 Thread-Programmierung

Aufwand zur Thread-Erzeugung unabhangig von der Pro-blemgroße und relativ gering ist, dass aber auf der anderenSeite Tasks dynamisch erzeugt werden konnen und so auchadaptive und irregulare Anwendungen effizient abgearbei-tet werden konnen.

Task−

pool

Task−Task−

ablageTask−

entn

ahme

Task−ablage

entnahme

Task−

entnahme

Task−Task−

ablag

eablage

Task−

entnahme

Thread 2

Thre

ad 1 Thread 3

Thre

ad 4

Daten−

puffer

Ablage

Ablage

Entnah

me

Entnahme

Konsument 2

Konsument 3

Konsument 1

Produzent 2

Produzent 1

Produzent 3

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-Threadsals Eingabe genutzt werden. Fur die Ubergabe der Datenwird eine gemeinsame Datenstruktur vorgegebener Langeals Puffer benutzt, auf die beide Threadtypen schreibendbzw. lesend zugreifen. Die Produzenten-Threads legen dievon ihnen erzeugten Eintrage in den Puffer ab, die Kon-sumenten-Threads entnehmen Eintrage aus dem Puffer undverarbeiten diese weiter, siehe Abb. 3.7. Die Produzentenkonnen nur Eintrage im Puffer ablegen, wenn dieser nichtvoll ist. Entsprechend konnen die Konsumenten nur Ein-trage entnehmen, wenn der Puffer nicht leer ist. Zum kor-rekten Ablauf ist fur den Zugriff auf die Pufferdatenstruktureine Synchronisation der zugreifenden Threads erforderlich.

Page 69: Multicore Parallele Programmierung Kng617

3.5 Parallele Programmierumgebungen 61

3.5 Parallele Programmierumgebungen

Fur die parallele Programmierung steht eine Vielzahl vonUmgebungen zur Verfugung. Die verbreitetsten sind:Posix Threads: Posix Threads (auch Pthreads genannt)ist eine portable Thread-Bibliothek, die fur viele Betriebs-systeme nutzbar ist. Mittlerweile ist Pthreads die Standard-Schnittstelle fur Linux und wird auch fur Unix-Plattformenhaufig genutzt. Fur Windows ist eine Open-Source-Versionpthreads-win32 verfugbar. Die Programmiersprache ist C.Kapitel 4 stellt Pthreads detaillierter vor.Win32/MFC Thread API: Das Win32/MFC API bietetdem Softwareentwickler eine C/C++-Umgebung zur Ent-wicklung von Windows-Anwendungen. Es werden Mecha-nismen zur Erzeugung und Verwaltung von Threads zurVerfugung gestellt sowie Kommunikations- und Synchroni-sations-Mechanismen. Wir verweisen u.a. auf [3] fur einegenauere Beschreibung.Threading API fur Microsoft.NET: Das .NET-Frame-work bietet umfangreiche Unterstutzung fur die Program-mierung mit Threads fur die Sprachen C++, Visual Basic,.NET, JScript.NET oder C#. Das Laufzeitsystem wird alsCommon Language Runtime (CLR) bezeichnet; CLR ar-beitet mit einer Zwischencodedarstellung ahnlich zu JavaBytecode, siehe [3] fur eine detailliertere Beschreibung.Java-Threads: Die Programmiersprache Java unterstutztdie Erzeugung, Verwaltung und Synchronisation von Thre-ads auf Sprachebene bzw. durch Bereitstellung speziellerKlassen und Methoden. Das Paket java.util.concurrent(ab Java 1.5) stellt eine Vielzahl zusatzlicher Synchronisa-tions-Mechanismen zur Verfugung. Kapitel 5 enthalt einedetailliertere Einfuhrung.OpenMP: OpenMP ist ein API zur Formulierung por-tierbarer Multithreading-Programme, das Fortran, C und

Page 70: Multicore Parallele Programmierung Kng617

62 3 Thread-Programmierung

C++ unterstutzt. Das Programmiermodell wird durch eineplattformunabhangige Menge von Compiler-Pragmas und-Direktiven, Funktionsaufrufen und Umgebungsvariablenrealisiert, 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 fur die Kommunikation zwischen Prozessenmit jeweils separatem Adressraum definiert und stellt eineVielzahl von Kommunikationsoperationen zur Verfugung,die sowohl Einzeltransfers (mit jeweils zwei Kommunika-tionspartnern) als auch globale Kommunikationsoperatio-nen wie Broadcast- oder Reduktionsoperationen umfassen.Sprachanbindungen wurden fur C, C++ und Fortran de-finiert, es gibt aber auch MPI-Implementierungen fur Ja-va. Obwohl MPI fur einen verteilten Adressraum entworfenwurde, kann es im Prinzip auch fur die Programmierungvon Multicore-Prozessoren mit gemeinsamem Adressraumverwendet werden. Dazu wird auf jedem Prozessorkern einseparater Prozess mit privaten Daten gestartet. Der Daten-bzw. Informationsaustausch zwischen den Prozessen erfolgtmit MPI-Kommunikationsoperationen, Zugriffe auf den ge-meinsamen Speicher und damit auch die damit verbunde-nen Synchronisationsoperationen entfallen. Im Vergleich zuThreads stellt dies ein vollig anderes Programmiermodelldar, das je nach Anwendungsprogramm aber durchaus zueiner ahnlichen Prozessorauslastung wie die Verwendung ei-nes Threadmodells fuhren kann.

Das MPI-Modell ist insbesondere fur solche Program-me geeignet, in denen jeder Berechnungsstrom auf einenihm zuzuordnenden Datenbereich zugreift und relativ seltenDaten anderer Datenbereiche braucht. Wir gehen im Fol-genden nicht naher auf MPI ein und verweisen auf [59, 26]fur eine detaillierte Beschreibung.

Page 71: Multicore Parallele Programmierung Kng617

4

Programmierung mit Pthreads

Posix Threads (auch Pthreads genannt) ist ein Standardzur Programmierung im Threadmodell mit der Program-miersprache C. Dieser Abschnitt fuhrt in den Pthreads-Standard kurz ein; vollstandigere Behandlungen sind in[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 somitin jedes Pthreads-Programm eingebunden werden muss. Al-le Pthreads-Funktionen liefern den Wert 0 zuruck, wennsie fehlerfrei durchgefuhrt werden konnten. Wenn bei derDurchfuhrung ein Fehler aufgetreten ist, wird ein Fehlerco-de aus <error.h> zuruckgegeben. Diese Headerdatei solltedaher 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-

Page 72: Multicore Parallele Programmierung Kng617

64 4 Programmierung mit Pthreads

gramms ausfuhrt. Ein Thread ist in der Pthreads-Bibliothekdurch den Typ pthread t dargestellt. Der Haupt-Threadkann 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 aufein Datenobjekt vom Typ pthread t. In diesem Argumentwird eine Identifikation des erzeugten Threads ablegt, dieauch als Thread-Name (thread identifier, TID) bezeichnetwird und mit der dieser Thread in nachfolgenden Aufru-fen von Pthreads-Funktionen angesprochen werden kann.Das zweite Argument ist ein Zeiger auf ein Attributobjektvom Typ pthread attr t, mit dessen Hilfe das Verhaltendes Threads (wie z.B. Scheduling, Prioritaten, Große desLaufzeitstacks) beeinflusst werden kann. Die Angabe vonNULL bewirkt, dass ein Thread mit den Default-Attributenerzeugt wird. Sollen die Attribute abweichend gesetzt wer-den, muss die Attributdatenstruktur vor dem Aufruf vonpthread create() entsprechend besetzt werden. Ublicher-weise reicht die Verwendung der Default-Attribute aus. Dasdritte Argument bezeichnet die Funktion start routine(),die der Thread nach seiner Erzeugung ausfuhrt. Diese Funk-tion hat ein einziges Argument vom Typ void * und lie-fert einen Wert des gleichen Typs zuruck. Das vierte Argu-ment ist ein Zeiger auf das Argument, mit dem die Funktionstart routine() ausgefuhrt werden soll.

Um mehrere Argumente an die Startfunktion eines Thre-ads zu ubergeben, mussen diese in eine Datenstruktur ge-packt werden, deren Adresse an die Funktion ubergebenwird. Sollen mehrere Threads die gleiche Funktion mitunterschiedlichen Argumenten ausfuhren, so sollte jedem

Page 73: Multicore Parallele Programmierung Kng617

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 fruh uberschrieben werdenoder dass verschiedene Threads ihre Argumentwerte kon-kurrierend verandern.

Ein Thread wird beendet, indem er die auszufuhrendeStartfunktion vollstandig abarbeitet oder aber die Biblio-theksfunktion

void pthread exit (void *valuep)

aufruft, wobei valuep den Ruckgabewert bezeichnet, der anden aufrufenden Thread oder einen anderen Thread zuruck-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 Ruckgabewert der Startfunktionwird zuruckgegeben. Da nach dem Aufruf von pthread -exit() der aufgerufene Thread und damit auch der von ihmverwendete Laufzeitstack nicht mehr existiert, sollte fur denRuckgabewert valuep keine lokale Variable der Startfunk-tion oder einer anderen Funktion verwendet werden. Diesewerden auf dem Laufzeitstack aufgehoben und konnen nachdessen Freigabe durch einen anderen Thread uberschriebenwerden. Stattdessen sollte eine globale oder eine dynamischallokierte Variable verwendet werden.

Ein Thread kann auf die Beendigung eines anderenThreads 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 aufrufendeThread wird so lange blockiert, bis der angegebene Threadbeendet ist. Die Funktion pthread join bietet also eine

Page 74: Multicore Parallele Programmierung Kng617

66 4 Programmierung mit Pthreads

Moglichkeit der Synchronisation von Threads. Der Ruckga-bewert des beendeten Threads thread wird dem wartendenThread in der Variable valuep zuruckgeliefert.

Die Pthreads-Bibliothek legt fur jeden erzeugten Threadeine interne Datenstruktur an, die die fur die Abarbeitungdes Threads notwendigen Informationen enthalt. Diese Da-tenstruktur wird von der Bibliothek auch nach Beendigungeines Threads aufgehoben, damit ein anderer Thread einepthread join()-Operation erfolgreich durchfuhren kann.Durch den Aufruf von pthread join() wird auch die in-terne Datenstruktur des angegebenen Threads freigegebenund kann danach nicht mehr verwendet werden.

4.2 Koordination von Threads

Die Threads eines Prozesses teilen sich einen gemeinsamenAdressraum und konnen daher konkurrierend auf gemein-same Variablen zugreifen. Um dabei zeitkritische Ablaufezu vermeiden, mussen die Zugriffe der beteiligten Thre-ads koordiniert werden. Als wichtigste Hilfsmittel stellenPthreads-Bibliotheken Mutexvariablen und Bedingungsva-riablen zur Verfugung.

Eine Mutexvariable bezeichnet eine Datenstruk-tur des vorgegebenen Typs pthread mutex t, die dazuverwendet werden kann, den wechselseitigen Ausschlussbeim Zugriff auf gemeinsame Variablen sicherzustellen. Ei-ne Mutexvariable kann zwei Zustande annehmen: gesperrt(locked) und ungesperrt (unlocked). Um einen wechselseiti-gen Ausschluss beim Zugriff auf gemeinsame Datenstruktu-ren sicherzustellen, mussen die beteiligten Threads jeweilsfolgendes Verhalten aufweisen: Bevor ein Thread eine Mani-pulation der gemeinsamen Datenstruktur startet, sperrt erdie zugehorige Mutexvariable mit einem speziellen Funkti-

Page 75: Multicore Parallele Programmierung Kng617

4.2 Koordination von Threads 67

onsaufruf. Wenn ihm dies gelingt, ist er der Eigentumer derMutexvariable und er hat die Kontrolle uber sie. Nach Be-endigung der Manipulation der gemeinsamen Datenstruk-tur gibt der Thread die Sperre der Mutexvariable wiederfrei.

Versucht ein Thread die Kontrolle uber eine von einemanderen Thread kontrollierte Mutexvariable zu erhalten,wird er so lange blockiert, bis der andere Thread die Mu-texvariable wieder freigegeben hat. Die Thread-Bibliothekstellt 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 jedochein Thread die Datenstruktur manipuliert ohne vorher dieKontrolle uber die Mutexvariable erhalten zu haben, ist einwechselseitiger Ausschluss nicht mehr garantiert.

Die Zuordnung zwischen einer Mutexvariablen und derihr zugeordneten Datenstruktur erfolgt implizit dadurch,dass die Zugriffe auf die Datenstruktur durch Sperren bzw.Freigabe der Mutexvariablen geschutzt werden; eine expli-zite Zuordnung existiert nicht. Die Lesbarkeit eines Pro-gramms kann jedoch dadurch erleichtert werden, dass dieDatenstruktur und die fur deren Kontrolle verwendete Mu-texvariable in einer gemeinsamen Struktur gespeichert wer-den. Mutexvariablen konnen wie alle anderen Variablen de-klariert oder dynamisch erzeugt werden. Bevor eine Mu-texvariable benutzt werden kann, muss sie durch Aufrufder Funktion

int pthread mutex init (pthread mutex t *mutex,

const pthread mutexattr t *attr)

initialisiert werden. Fur attr = NULL wird eine Mutexva-riable mit den Default-Eigenschaften zur Verfugung ge-

Page 76: Multicore Parallele Programmierung Kng617

68 4 Programmierung mit Pthreads

stellt. Eine statisch deklarierte Mutexvariable mutex kannauch 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 zerstort werden. Eine Mutexvariable sollte erst dannzerstort werden, wenn kein Thread mehr auf ihre Freigabewartet. Eine zerstorte Mutexvariable kann durch eine er-neute Initialisierung weiterverwendet werden.

Ein Thread erhalt die Kontrolle uber eine Mutexvaria-ble, indem er diese durch Aufruf der Funktion

int pthread mutex lock (pthread mutex t *mutex)

sperrt. Wird die angegebene Mutexvariable mutex bereitsvon einem anderen Thread kontrolliert, so wird der nun dieFunktion pthread mutex lock() aufrufende Thread blo-ckiert, bis der momentane Eigentumer die Mutexvariablewieder freigibt. Wenn mehrere Threads versuchen, die Kon-trolle uber eine Mutexvariable zu erhalten, werden die aufderen Freigabe wartenden Threads in einer Warteschlangegehalten. Welcher der wartenden Threads nach der Freiga-be der Mutexvariable zuerst die Kontrolle uber diese erhalt,kann von den Prioritaten der wartenden Threads und demverwendeten Scheduling-Verfahren abhangen. Ein Threadkann eine von ihm kontrollierte Mutexvariable mutex durchAufruf der Funktion

int pthread mutex unlock (pthread mutex t *mutex)

Page 77: Multicore Parallele Programmierung Kng617

4.2 Koordination von Threads 69

wieder freigeben. Wartet zum Zeitpunkt des Aufrufs vonpthread mutex unlock() kein anderer Thread auf die Frei-gabe der Mutexvariable, so hat diese nach dem Aufruf kei-nen Eigentumer mehr. Wenn andere Threads auf die Frei-gabe der Mutexvariable warten, wird einer dieser Threadsaufgeweckt und Eigentumer der Mutexvariablen.

In manchen Situationen ist es sinnvoll, dass ein Threadfeststellen kann, ob eine Mutexvariable von einem anderenThread kontrolliert wird, ohne dass er dadurch blockiertwird. Dazu steht die Funktion

int pthread mutex trylock (pthread mutex t *mutex)

zur Verfugung. Beim Aufruf dieser Funktion erhalt deraufrufende Thread die Kontrolle uber die Mutexvariablemutex, wenn diese frei ist. Wenn diese zur Zeit von ei-nem anderen Thread gesperrt ist, liefert der Aufruf EBUSYzuruck; dies fuhrt aber nicht wie beim Aufruf von pthread -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 ererfolgreich ist (spinlock).

Beim gleichzeitigen Sperren mehrerer Mutexvariablendurch mehrere Threads besteht die Gefahr, dass Deadlocksauftreten, siehe Kapitel 3. Das Auftreten von Deadlockskann durch Verwenden einer festen Sperr-Reihenfolge oderdas 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 fur eine gesamte Funktion sichergestellt, wird sieals thread-sicher bezeichnet. Eine thread-sichere Funkti-on kann also gleichzeitig von mehreren Threads aufgerufenwerden, ohne dass die beteiligten Threads zur Vermeidung

Page 78: Multicore Parallele Programmierung Kng617

70 4 Programmierung mit Pthreads

von zeitkritischen Ablaufen zusatzliche Operationen beimFunktionsaufruf ausfuhren mussen.

Im Prinzip konnen Mutexvariablen jedoch auch dazuverwendet werden, auf das Eintreten einer Bedingung zuwarten, die vom Zustand globaler Datenstrukturen abhangt.Dazu verwendet der zugreifende Thread eine oder mehrereMutexvariablen zum Schutz des Zugriffs auf die globalenDaten und wertet die gewunschte Bedingung von Zeit zuZeit aus, indem er mit Hilfe der Mutexvariablen geschutztauf die entsprechenden globalen Daten zugreift. Wenn dieBedingung erfullt ist, kann der Thread die beabsichtigteOperation ausfuhren. Diese Vorgehensweise hat den Nach-teil, dass der auf das Eintreten der Bedingung warten-de Thread die Bedingung evtl. sehr oft auswerten muss,bis diese erfullt ist, und dabei CPU-Zeit verbraucht (ak-tives Warten). Um diesen Nachteil zu beheben, stellt derPthreads-Standard Bedingungsvariablen zur Verfugung.

4.3 Bedingungsvariablen

Eine Bedingungsvariable ist eine Datenstruktur, die eseinem Thread erlaubt, auf das Eintreten einer beliebigenBedingung zu warten. Fur Bedingungsvariablen wird einSignalmechanismus zur Verfugung gestellt, der den warten-den Thread wahrend der Wartezeit blockiert, so dass erkeine CPU-Zeit verbraucht, und wieder aufweckt, sobalddie angegebene Bedingung erfullt ist. Um diesen Mecha-nismus zu verwenden, muss der ausfuhrende Thread nebender Bedingungsvariablen einen Bedingungsausdruck an-geben, der die Bedingung bezeichnet, auf deren Erfullungder Thread wartet. Eine Mutexvariable wird verwendet,um die Auswertung des Bedingungsausdrucks zu schutzen.Letzteres ist notwendig, da der Bedingungsausdruck in der

Page 79: Multicore Parallele Programmierung Kng617

4.3 Bedingungsvariablen 71

Regel auf globale Datenstrukturen zugreift, die von anderenThreads konkurrierend verandert werden konnen.

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 durchAufruf 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-Datenstrukturfur Bedingungsvariablen. Fur attr = NULL erfolgt eine In-itialisierung mit den Default-Attributen. Die Initialisierungkann auch bei der Deklaration einer Bedingungsvariablendurch Verwendung eines Makros erfolgen:

pthread cond t cond = PTHREAD COND INITIALIZER.

Eine mit pthread cond init() dynamisch initialisierteBedingungsvariable cond sollte durch Aufruf der Funktion

int pthread cond destroy (pthread cond t *cond)

zerstort werden, wenn sie nicht mehr gebraucht wird, damitdas Laufzeitsystem die fur die Bedingungsvariable abgeleg-te Information freigeben kann. Statisch initialisierte Bedin-gungsvariablen mussen nicht freigegeben werden.

Eine Bedingungsvariable muss eindeutig mit einer Mu-texvariablen assoziiert sein. Alle Threads, die zur gleichenZeit auf die Bedingungsvariable warten, mussen die glei-che Mutexvariable verwenden, d.h. fur eine Bedingungs-variable durfen von verschiedenen Threads nicht verschie-dene Mutexvariablen verwendet werden. Eine Mutexvaria-ble kann jedoch verschiedenen Bedingungsvariablen zuge-ordnet werden. Nach dem Sperren der Mutexvariablen mit

Page 80: Multicore Parallele Programmierung Kng617

72 4 Programmierung mit Pthreads

pthread mutex lock() kann ein Thread durch Aufruf derFunktion

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 miteiner Bedingung assoziiert sein, da sonst die Gefahr vonDeadlocks oder zeitkritischen Ablaufen vorliegt [11]. Dietypische 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 demAufruf von pthread cond wait() unter dem Schutz derMutexvariablen mutex ausgefuhrt, um sicherzustellen, dassdie Bedingung sich zwischen ihrer Auswertung und demAufruf von pthread cond wait() nicht durch Berechnun-gen anderer Threads verandert. Daher muss durch das Pro-gramm auch gewahrleistet sein, dass jeder andere Threadeine Manipulation einer in den Bedingungen auftreten-den gemeinsamen Variable mit der gleichen Mutexvariablenschutzt.

• Wenn bei der Ausfuhrung des Programmsegments dieangegebene Bedingung erfullt ist, wird die pthread -cond wait()-Funktion nicht aufgerufen, und der aus-fuhrende Thread arbeitet nach pthread mutex unlock()das nachfolgende Programm weiter ab.

• Wenn dagegen die Bedingung nicht erfullt ist, wirdpthread cond wait() aufgerufen mit dem Effekt, dass

Page 81: Multicore Parallele Programmierung Kng617

4.3 Bedingungsvariablen 73

der ausfuhrende Thread T1 gleichzeitig die Kontrolleuber die Mutexvariable freigibt und so lange bezuglichder Bedingungsvariable blockiert, bis er von einem an-deren Thread T2 mit einer pthread cond signal()-Anweisung aufgeweckt wird, siehe unten. Wird ThreadT1 durch diese Anweisung wieder aufgeweckt, versuchter automatisch, die Kontrolle uber die Mutexvariablemutex zuruckzuerhalten. Hat bereits ein anderer ThreadKontrolle uber die Mutexvariable mutex, so wird deraufgeweckte Thread T1 unmittelbar nach dem Aufwe-cken so lange bzgl. der Mutexvariable blockiert, bis erdiese sperren kann. Erst wenn der aufgeweckte Threaddie Mutexvariable erfolgreich gesperrt hat, kann er mitder Abarbeitung seines Programms fortfahren, was zu-nachst die erneute Abarbeitung der Bedingung ist.

Das Programm sollte sicherstellen, dass der blockier-te Thread nur dann aufgeweckt wird, wenn die angegebe-ne Bedingung erfullt ist. Trotzdem ist es sinnvoll, die Be-dingung nach dem Aufwecken noch einmal zu uberprufen,da ein gleichzeitig aufgeweckter oder zeitgleich arbeitenderThread, der die Kontrolle uber die Mutexvariable zuersterhalt, die Bedingung oder in der Bedingung enthaltene ge-meinsame Daten modifizieren kann und so die Bedingungnicht mehr erfullt ist.

Zum Aufwecken von bzgl. einer Bedingungsvariable war-tenden Threads stehen die beiden folgenden Funktionen zurVerfugung:

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, wenndie Bedingung erfullt ist. Wartet kein Thread, so hat der

Page 82: Multicore Parallele Programmierung Kng617

74 4 Programmierung mit Pthreads

Aufruf keinen Effekt. Warten mehrere Threads, wird einThread anhand der Prioritaten der Threads und der ver-wendeten Scheduling-Strategie ausgewahlt. Ein Aufruf derFunktion pthread cond broadcast() weckt alle bzgl. derBedingungsvariablen cond wartenden Threads auf. Dabeikann aber hochstens einer dieser Threads die Kontrol-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 Verfugung. Der Unterschied zu pthread cond wait()besteht darin, dass die Blockierung bzgl. der Bedingungsva-riable cond aufgehoben wird, wenn die in time angegebeneabsolute Zeit abgelaufen ist. In diesem Fall wird die Fehler-meldung ETIMEDOUT zuruckgeliefert.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 diezusatzliche Anzahl von Nanosekunden der verwendeten Zeit-scheiben angibt. Der Parameter time von pthread cond -timedwait() gibt eine absolute Tageszeit und kein relativesZeitintervall an.Eine typische Benutzung ist in Abbildung 4.1 angegeben.In diesem Beispiel wartet der ausfuhrende Thread maxi-mal zehn Sekunden auf das Eintreten der Bedingung. Zur

Page 83: Multicore Parallele Programmierung Kng617

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 zuruck, die seit dem 1. Januar 1970vergangen ist.) Wenn die Bedingung nach zehn Sekundennoch nicht erfullt ist, wird die Funktion timed out work()ausgefuhrt, und die Bedingung wird erneut uberpruft.

4.4 Erweiterter Sperrmechanismus

Bedingungsvariablen konnen dazu verwendet werden, kom-plexere Synchronisationsmechanismen zu implementieren.Als Beispiel hierfur betrachten wir im Folgenden einenLese/Schreib-Sperrmechanismus, der als Erweiterungdes von Mutexvariablen zur Verfugung gestellten Sperrme-chanismus aufgefasst werden kann. Wird eine gemeinsameDatenstruktur von einer normalen Mutexvariable geschutzt,so kann zu einem Zeitpunkt jeweils nur ein Thread diegemeinsame Datenstruktur lesen bzw. auf die gemeinsa-me Datenstruktur schreiben. Die Idee des Lese/Schreib-

Page 84: Multicore Parallele Programmierung Kng617

76 4 Programmierung mit Pthreads

Sperrmechanismus besteht darin, dies dahingehend zu er-weitern, dass zum gleichen Zeitpunkt eine beliebige Anzahlvon 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]. Fur eine komplexere undeffizientere Implementierung verweisen wir auf [11, 35].

Fur die Implementierung des erweiterten Sperrmecha-nismus werden RW-Sperrvariablen (read/write lock va-riables) verwendet, die mit Hilfe einer Mutex- und einerBedingungsvariablen wie folgt definiert werden konnen:

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 hochstens denWert Eins. Die Mutexvariable soll diese Zahler der Lese-und Schreibzugriffe schutzen. Die Bedingungsvariable re-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 derInitialisierung einer RW-Sperrvariable vom Typ rw lock t.Die Funktion rw lock rlock() fordert einen Lesezugriffauf die gemeinsame Datenstruktur an. Der Lesezugriff wirdnur dann gewahrt, wenn kein anderer Thread eine Schreib-berechtigung erhalten hat. Hat ein anderer Thread ei-ne Schreibberechtigung, wird der anfordernde Thread blo-ckiert, bis die Schreibberechtigung wieder zuruckgegebenwird. Die Funktion rw lock wlock() dient der Anforde-

Page 85: Multicore Parallele Programmierung Kng617

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).

Page 86: Multicore Parallele Programmierung Kng617

78 4 Programmierung mit Pthreads

rung einer Schreibberechtigung. Diese wird nur gewahrt,wenn kein anderer Thread eine Lese- oder eine Schreibbe-rechtigung erhalten hat.

Die Funktion rw lock runlock() dient der Ruckgabeeiner Leseberechtigung. Sinkt durch die Ruckgabe einer Le-seberechtigung die Anzahl der lesend zugreifenden Threadsauf 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 Ruckgabe einer Schreibberechtigung. Da ma-ximal ein schreibender Thread erlaubt ist, hat nach dieserRuckgabe kein Thread mehr eine Schreibberechtigung, undalle auf einen Lesezugriff wartenden Threads konnen durchpthread cond broadcast() aufgeweckt werden.

Die skizzierte Implementierung von RW-Sperrvariablengibt Lesezugriffen Vorrang vor Schreibzugriffen: Wenn einThread T1 eine Leseerlaubnis erhalten hat und Thread T2

auf eine Schreiberlaubnis wartet, erhalten andere Threadsauch dann eine Leseerlaubnis, wenn diese nach der Schrei-berlaubnis von T2 beantragt wird. Thread T2 erhalt erstdann eine Schreiberlaubnis, wenn kein anderer Thread mehreine Leseerlaubnis beantragt hat. Je nach Anwendung kannes sinnvoll sein, den Schreibzugriffen Vorrang vor Lesezu-griffen zu geben, damit die Datenstruktur immer auf demaktuellsten 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, fur jede abzuarbeitende Aufgabe oder Funkti-on (also allgemein Task) genau einen Thread zu erzeugen,der diese Task abarbeitet und anschließend wieder zerstort

Page 87: Multicore Parallele Programmierung Kng617

4.5 Implementierung eines Taskpools 79

wird. Dies kann je nach Anwendung dazu fuhren, dass sehrviele Threads erzeugt und wieder zerstort werden, was einennicht unerheblichen Zeitaufwand verursachen kann, insbe-sondere wenn jeweils pro Task nur wenige Berechnungenauszufuhren sind. Eine effizientere parallele Implementie-rung kann mit Hilfe eines Taskpools erreicht werden, sieheauch Kapitel 3. Die Idee eines Taskpools besteht darin, eineDatenstruktur anzulegen, in der die noch abzuarbeitendenProgrammteile (Tasks) abgelegt sind. Fur die Abarbeitungder Tasks wird eine feste Anzahl von Threads verwendet,die zu Beginn des Programms vom Haupt-Thread erzeugtwerden und bis zum Ende des Programms existieren. Furdie Threads stellt der Taskpool eine gemeinsame Daten-struktur dar, auf die sie zugreifen und die dort abgelegtenTasks entnehmen und anschließend abarbeiten. Wahrendder Abarbeitung einer Task kann ein Thread neue Taskserzeugen und diese in den Taskpool einfugen. Die Abarbei-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 Implementierungensind zum Beispiel in [11, 35, 38, 30] beschrieben.

Abbildung 4.3 zeigt die Datenstruktur eines Taskpoolsund die Funktion tpool init() zur Initialisierung des Task-pools. Der Datentyp work t beschreibt eine einzelne Taskdes Taskpools. Diese Beschreibung besteht aus je einem Zei-ger auf die auszufuhrende Funktion und auf das Argumentdieser Funktion. Die einzelnen Tasks sind durch Zeiger nextin Form einer einfach verketteten Liste miteinander ver-bunden. Der Datentyp tpool t beschreibt die gesamte Da-tenstruktur eines Taskpools. Dabei bezeichnet num thr dieAnzahl der fur die Abarbeitung verwendeten Threads; dasFeld threads enthalt Zeiger auf die abarbeitenden Threads.Die Eintrage max size und current size geben die maxi-

Page 88: Multicore Parallele Programmierung Kng617

80 4 Programmierung mit Pthreads

male bzw. aktuelle Anzahl von eingetragenen Tasks an. DieZeiger head und tail zeigen auf den Anfang bzw. das Endeder Taskliste.

Die Mutexvariable lock wird verwendet, um den wech-selseitigen Ausschluss beim Zugriff auf den Taskpool durchdie Threads sicherzustellen. Wenn ein Thread versucht, ei-ne Task aus einem leeren Taskpool zu entnehmen, wird erbzgl. der Bedingungsvariable not empty blockiert. Fugt einThread einen Task in einen leeren Taskpool ein, wird einbzgl. der Bedingungsvariable not empty blockierter Threadaufgeweckt. Wenn ein Thread versucht, eine Task in einenvollen Taskpool einzufugen, wird er bzgl. der Bedingungsva-riable not full blockiert. Entnimmt ein Thread eine Taskaus 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 initialisiertund die zur Abarbeitung vorgesehene Anzahl von Threadstpl->threads[i], i=0,...,num thr-1, erzeugt. Jeder die-ser Threads erhalt eine Funktion tpool thread() als Star-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() wirdversucht, eine Task vom Anfang der Taskliste des Task-pools zu entnehmen. Wenn der Taskpool zur Zeit leer ist,wird der ausfuhrende Thread bzgl. der Bedingungsvaria-ble not empty blockiert. Sonst wird eine Task wl vomAnfang der Taskschlange entnommen. War der Taskpoolvor der Entnahme voll, werden alle Threads, die blockiertsind, weil sie eine Task abzulegen versuchen, mit einerpthread cond broadcast()-Anweisung aufgeweckt. Die Zu-griffe von tpool thread auf den Taskpool werden durch die

Page 89: Multicore Parallele Programmierung Kng617

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.

Page 90: Multicore Parallele Programmierung Kng617

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 geschutzt. Die Abarbeitung der Funk-tion routine einer entnommenen Task wl wird danachausgefuhrt. Diese Abarbeitung kann die Erzeugung neuerTasks beinhalten, die durch die Funktion tpool insert inden Taskpool tpl eingetragen werden.

Die Funktion tpool insert() in Abbildung 4.5 fugt ei-ne Task in den Taskpool ein. Falls der Taskpool voll ist,wird der ausfuhrende Thread bzgl. der Bedingungsvariablenot full blockiert. Ist der Taskpool nicht voll, so wird eineTask mit den entsprechenden Daten belegt und an das En-de der Taskschlange gehangt. War diese vor dem Anhangen

Page 91: Multicore Parallele Programmierung Kng617

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 Bedingungsvariablenot empty blockiert ist, aufgeweckt. Die Manipulationendes Taskpools tpl werden wieder durch die Mutexvariablegeschutzt.

Die skizzierte Implementierung eines Taskpools ist ins-besondere fur ein Master-Slave-Modell geeignet, in dem einMaster-Thread mit tpool init() die gewunschte Anzahl

Page 92: Multicore Parallele Programmierung Kng617

84 4 Programmierung mit Pthreads

von Slave-Threads erzeugt, von denen jeder die Funktiontpool thread() abarbeitet. Die zu bearbeitenden Taskswerden entsprechend der zu realisierenden Anwendung de-finiert und konnen vom Master-Thread durch Aufruf vontpool insert() in den Taskpool eingetragen werden. Wer-den bei der Bearbeitung einer Task neue Tasks erzeugt,konnen diese auch vom ausfuhrenden Slave-Thread einge-tragen werden. Die Beendigung der Slave-Threads nachvollstandiger Abarbeitung aller Tasks wird vom Master-Thread ubernommen. Dazu werden alle bzgl. der beidenBedingungsvariablen not empty und not full blockiertenThreads aufgeweckt und beendet. Sollte ein Thread geradeeine Task bearbeiten, wird auf die Beendigung der Abar-beitung gewartet bevor der Thread beendet wird.

Page 93: Multicore Parallele Programmierung Kng617

5

Java-Threads

Die Entwicklung von aus mehreren Threads bestehendenProgrammen wird in der objektorientierten Programmier-sprache Java auf Sprachebene unterstutzt. Java stellt dazuu.a. Sprachkonstrukte fur die synchronisierte Ausfuhrungvon Programmbereichen bereit und erlaubt die Erzeugungund Verwaltung von Threads durch Verwendung vordefi-nierter Klassen. Im Folgenden wird die Verwendung vonJava-Threads zur Entwicklung paralleler Programme fureinen gemeinsamen Adressraum kurz vorgestellt, wobei nurauf fur Threads wesentliche Aspekte eingegangen wird. Fureine ausfuhrliche Behandlung der Programmiersprache Ja-va verweisen wir auf [22].

5.1 Erzeugung von Threads in Java

Jedes ausgefuhrte Java-Programm besteht aus mindestenseinem Thread, dem Haupt-Thread. Dieses ist der Thread,der die main()-Methode der Klasse ausfuhrt, die als Star-targument der Java Virtual Machine (JVM) angegeben

Page 94: Multicore Parallele Programmierung Kng617

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 KlasseThread aus dem Standardpaket java.lang zur Verfugung,die zur Reprasentation von Threads verwendet wird unddie Mechanismen und Methoden zur Erzeugung und Ver-waltung von Threads bereitstellt. Das Interface Runnableaus java.lang reprasentiert den von einem Thread aus-zufuhrenden Code, der in einer run()-Methode zur Verfu-gung gestellt wird. Fur die Definition einer run()-Methode,die von einem Thread asynchron ausgefuhrt wird, gibt eszwei Moglichkeiten: das Erben von der Klasse Thread oderdie 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 unddie enthaltene Methode run() mit den Anweisungen desauszufuhrenden Threads uberschreibt. Zusatzlich enthaltdie Klasse Thread eine Methode start(), die einen neu-en Thread erzeugt, der dann die Methode run() ausfuhrt.Der neu erzeugte Thread wird asynchron zum aufrufendenThread ausgefuhrt. Nach Ausfuhrung von start() wird dieKontrolle direkt an den aufrufenden Thread zuruckgege-ben. Dies erfolgt evtl. vor der Beendigung des neu erzeugtenThreads, so dass erzeugender und erzeugter Thread asyn-chron zueinander arbeiten. Der neu erzeugte Thread termi-niert, sobald seine run()-Methode vollstandig abgearbeitetist.

Dieses Vorgehen ist in Abbildung 5.1 am Beispiel ei-ner Klasse NewClass illustriert, deren main-Methode einObjekt der Klasse NewClass erzeugt und dessen run()-Methode durch Aufruf von start aktiviert wird.

Page 95: Multicore Parallele Programmierung Kng617

5.1 Erzeugung von Threads in Java 87

import java.lang.Thread;

public class NewClass extends Thread { // Vererbung

public void run() {// Uberschreiben von run() der Thread-Klasse

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 Uberschreibender run()-Methode der Klasse Thread.

Bei der gerade beschriebenen Methode zur Erzeugungeines Threads muss die neue Klasse von der Klasse Threaderben. Da Java keine Mehrfach-Vererbung zulasst, hat diesden Nachteil, dass die neue Klasse von keiner weiterenKlasse erben kann, was die Entwicklung von Anwendungs-programmen einschrankt. Dieser Nachteil der fehlendenMehrfach-Vererbung wird in Java durch die Bereitstel-lung von Interfaces ausgeglichen, wofur im Falle der KlasseThread das Interface Runnable genutzt wird.

Verwendung des Interface Runnable

Das Interface Runnable enthalt eine parameterlose run()-Methode:

public interface Runnable {public abstract void run();

}

Page 96: Multicore Parallele Programmierung Kng617

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 KlasseThread abgeleitet zu werden. Objekte einer solchen KlasseNewClass sind aber keine Threadobjekte, so dass zur Er-zeugung eines Threads immer noch ein Objekt der KlasseThread erzeugt werden muss, das allerdings als Parameterein Objekt der neuen Klasse NewClass hat. Dazu enthaltdie 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 erzeugtenThread asynchron zum aufrufenden Thread ausgefuhrt. DieErzeugung eines neuen Threads kann somit in drei Schrittenerfolgen:

(1) Definition einer neuen Klasse NewClass, die Runnableimplementiert und fur die eine run()-Methode definiertwird, die die von dem neu zu erzeugenden Thread aus-zufuhrende Anweisungsfolge enthalt;

(2) Erzeugung eines Objektes der Klasse Thread mit Hilfedes 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.

Page 97: Multicore Parallele Programmierung Kng617

5.1 Erzeugung von Threads in Java 89

Dieses Vorgehen ist in Abbildung 5.2 am Beispiel einerKlasse NewClass illustriert. Ein Objekt dieser Klasse wirddem 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 InterfaceRunnable und Verwendung einer neuen Klasse NewClass.

Weitere Methoden der Klasse Thread

Ein Java-Thread kann auf die Beendigung eines anderenJava-Threads t warten, indem er t.join() aufruft. Durchdiesen Aufruf blockiert der aufrufende Thread so lange, bisder Thread t beendet ist. Die join()-Methode wird in dreiVarianten zur Verfugung gestellt:

• void join(): der aufrufende Thread wird blockiert, bisder angegebene Thread beendet ist;

• void join(long timeout): der aufrufende Thread wirdblockiert; die Blockierung wird aufgehoben, sobald der

Page 98: Multicore Parallele Programmierung Kng617

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 Verhaltenentspricht dem von void join(long timeout); der zu-satzliche Parameter ermoglicht eine genauere Angabedes Zeitintervalls durch die zusatzliche Angabe von Na-nosekunden.

Wurde der angegebene Thread noch nicht gestartet, findetbei keiner der join()-Varianten eine Blockierung statt.

Die Methode

boolean isAlive()

der Klasse Thread ermoglicht die Abfrage des Ausfuhrungs-status eines Threads: die Methode liefert true zuruck, fallsder angegebene Thread gestartet wurde, aber noch nichtbeendet 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 derausfuhrende Thread ist betroffen.

Die Thread-Klasse definiert einige statische Methoden,die den aktuell ausgefuhrten Thread betreffen oder Infor-mationen uber das Gesamtprogramm liefern.Da diese Methoden statisch sind, konnen sie aufgerufen wer-den, auch wenn kein Objekt der Klasse Thread verwendetwird. Der Aufruf der Methode

static Thread currentThread();

liefert eine Referenz auf das Thread-Objekt des aufrufendenThreads. Diese Referenz kann z.B. dazu verwendet werden,nicht-statische Methoden dieses Thread-Objektes aufzuru-fen. Die Methode

static void sleep (long milliseconds);

Page 99: Multicore Parallele Programmierung Kng617

5.2 Synchronisation von Java-Threads 91

blockiert den ausfuhrenden Thread vorubergehend fur dieangegebene Anzahl von Millisekunden, d.h. der Prozessorkann einem anderen Thread zugeteilt werden. Nach Ablaufdes Zeitintervalls wird der Thread wieder ausfuhrungsbereitund kann wieder einem Prozessor zur weiteren Ausfuhrungzugeteilt werden. Die Methode

static void yield();

ist ein Hinweis an die Java Virtual Machine (JVM), dass einanderer ausfuhrungsbereiter Thread gleicher Prioritat demProzessor zugeteilt werden soll. Wenn ein solcher Threadexistiert, kann der Scheduler der JVM diesen zur Ausfuh-rung bringen. Die Anwendung von yield() ist sinnvoll furJVM-Implementierungen ohne Scheduling mit Zeitschei-benverfahren, falls Threads langlaufende Berechnungen oh-ne Blockierungsmoglichkeit ausfuhren. Die Methode

static int enumerate (Thread[] th_array);

liefert eine Liste aller Thread-Objekte des Programms.Der Ruckgabewert gibt die Anzahl der im Parameterfeldth 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 Große desParameterfeldes 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, mussen also zur Ver-

Page 100: Multicore Parallele Programmierung Kng617

92 5 Java-Threads

meidung zeitkritischer Ablaufe geeignete Synchronisations-mechanismen angewendet werden. Zur Sicherstellung deswechselseitigen Ausschlusses von Threads beim Zugriff aufgemeinsame Daten stellt Java synchronized-Blocke und-Methoden zur Verfugung. Wird ein Block oder eine Me-thode als synchronized deklariert, ist sichergestellt, dasskeine gleichzeitige Ausfuhrung durch zwei Threads erfol-gen kann. Eine Datenstruktur kann also dadurch vor kon-kurrierenden Zugriffen mehrerer Threads geschutzt werden,dass alle Zugriffe auf die Datenstruktur in synchronizedMethoden oder Blocken erfolgen. Die synchronisierte Inkre-mentierung eines Zahlers kann beispielsweise durch folgendeMethode 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 Objectbesitzt eine solche implizite Mutexvariable. Da jede Klassedirekt oder indirekt von der Klasse Object abgeleitet ist,besitzt somit jedes Objekt eine Mutexvariable. Der Aufrufeiner synchronized-Methode bezuglich eines Objektes Obhat den folgenden Effekt:

• Beim Start der synchronized-Methode durch einenThread t wird die Mutexvariable von Ob implizit be-legt. Wenn die Mutexvariable bereits von einem anderenThread belegt ist, wird der ausfuhrende Thread t blo-ckiert. Der blockierte Thread wird wieder ausfuhrungs-

Page 101: Multicore Parallele Programmierung Kng617

5.2 Synchronisation von Java-Threads 93

bereit, wenn die Mutexvariable freigegeben wird. Dieaufgerufene synchronized-Methode wird nur bei erfolg-reicher Sperrung der Mutexvariablen von Ob ausgefuhrt.

• Beim Verlassen der Methode wird die Mutexvariablevon Ob implizit wieder freigegeben und kann damit voneinem 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 konnen,als synchronized deklariert werden. Zur Sicherstellungdes wechselseitigen Ausschlusses ist es wichtig, dass nuruber diese Methoden auf das zu schutzende Objekt zuge-griffen wird. Neben synchronized-Methoden konnen auchsynchronized-Blocke verwendet werden. Dies ist dann sinn-voll, wenn nur ein Teil einer Methode auf kritische Datenzugreift, eine synchronisierte Ausfuhrung der gesamten Me-thode aber nicht notwendig erscheint. Bei synchronized-Blocken erfolgt die Synchronisation meist bezuglich des Ob-jektes, in dessen Methode der synchronized-Block steht.Die obige Methode zur Inkrementierung eines Zahlers kannmit 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 atomareObjekte genannt, verwendet werden, die von einer beliebi-gen Anzahl von Threads ohne Synchronisation zugegriffenwerden konnen. Damit dabei keine zeitkritischen Ablaufeentstehen, muss die Synchronisation in der definierenden

Page 102: Multicore Parallele Programmierung Kng617

94 5 Java-Threads

Klasse enthalten sein. Diese muss folgende Bedingungenerfullen:

• alle Methoden mussen synchronized sein,• es durfen keine public-Felder enthalten sein, die ohne

Aufruf einer Methode zugegriffen werden konnen,• 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-synchronisierterObjekte am Beispiel einer Klasse ExpandableArray, die ei-ne vereinfachte Version der vordefinierten synchronisiertenKlasse java.util.Vector ist, vgl. auch [40]. Die Klasserealisiert ein adaptierbares Feld mit beliebigen Objekten,dessen Große entsprechend der Anzahl abgelegter Objek-te wachsen oder schrumpfen kann. Dies ist in der Methodeadd() realisiert: wird beim Hinzufugen eines neuen Ele-mentes festgestellt, dass das Feld data voll belegt ist, wirddieses entsprechend vergroßert. Dazu wird ein großeres Feldneu angelegt und das bisherige Feld wird mit Hilfe der Me-thode arraycopy() aus der System-Klasse umkopiert. Oh-ne die Synchronisationsoperationen konnte die Klasse nichtsicher von mehreren Threads gleichzeitig genutzt werden.Ein Konflikt konnte z.B. auftreten, wenn zwei Threads zumgleichen Zeitpunkt versuchen, eine add-Operation durch-zufuhren.

Auftreten von Deadlocks

Die Verwendung voll synchronisierter Klassen vermeidetzwar das Auftreten zeitkritischer Ablaufe, es konnen aberDeadlocks auftreten, wenn Threads bzgl. mehrerer Objekte

Page 103: Multicore Parallele Programmierung Kng617

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 fur eine voll synchronisierte Klasse.

Page 104: Multicore Parallele Programmierung Kng617

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 fur das Auftreten eines Deadlocks.

synchronisiert werden. Dies ist in Abb. 5.4 am Beispiel ei-nes Kontos (Klasse Account) veranschaulicht, bei dem dieMethode swapBalance() die Kontostande austauscht, vgl.auch [40]. Bei der Bearbeitung von swapBalance() ist beimEinsatz von zwei Threads T1 und T2 das Auftreten einesDeadlocks moglich, wenn ein Thread a.swapBalance(b),der andere Thread b.swapBalance(a) aufruft und die bei-den Threads auf unterschiedlichen Prozessorkernen einesProzessors ablaufen. Der Deadlock tritt bei folgender Ab-arbeitungsreihenfolge auf:

(A) Zeitpunkt 1: Thread T1 ruft a.swapBalance(b) aufund erhalt die Mutexvariable von Objekt a;

(B) Zeitpunkt 2: Thread T1 ruft getBalance() fur Objekta auf und fuhrt die Funktion aus;

(C) Zeitpunkt 2: Thread T2 ruft b.swapBalance(a) aufund erhalt die Mutexvariable von Objekt b;

Page 105: Multicore Parallele Programmierung Kng617

5.2 Synchronisation von Java-Threads 97

(D) Zeitpunkt 3: Thread T1 ruft b.getBalance() auf undblockiert bzgl. der Mutexvariable von Objekt b;

(E) Zeitpunkt 3: Thread T2 ruft getBalance() Fur Ob-jekt b auf auf fuhrt die Funktion aus;

(F) Zeitpunkt 4: Thread T2 ruft a.getBalance() auf undblockiert bzgl. der Mutexvariable von Objekt a.

Der Ablauf ist in Abb. 5.5 veranschaulicht. Zum Zeitpunkt4 sind beide Thread blockiert: Thread T1 ist bzgl. der Mu-texvariable von b blockiert. Diese ist von Thread T2 belegtund kann nur von Thread T2 freigegeben werden. ThreadT2 ist bzgl. der Mutexvariablen von a blockiert, die nur vonThread T1 freigegeben werden kann. Somit warten die bei-den Threads gegenseitig aufeinander und es ist ein Deadlockeingetreten.

Zeit- Thread T1 Thread T2

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 unddann b zu sperren, Thread T2 versucht das Sperren in um-gekehrter Reihenfolge. In dieser Situation kann das Auf-treten eines Deadlocks dadurch vermieden werden, dassdie beteiligten Threads die Objekte immer in der glei-chen Reihenfolge zu sperren versuchen. In Java kann diese

Page 106: Multicore Parallele Programmierung Kng617

98 5 Java-Threads

dadurch realisiert werden, dass die zu sperrenden Objek-te beim Sperren eindeutig angeordnet werden; dazu kannz.B. die Methode System.identityHashCode() verwendetwerden, die sich immer auf die Default-ImplementierungObject.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 konnen, vgl. Abb. 5.6. Die neue Formulierung enthaltauch eine Alias-Uberprufung, so dass die Operation nurausgefuhrt wird, wenn unterschiedliche Objekte beteiligtsind. Die Methode swapBalance() ist jetzt nicht mehr alssynchronized 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.

Page 107: Multicore Parallele Programmierung Kng617

5.2 Synchronisation von Java-Threads 99

Bei der Synchronisation von Java-Methoden sollten einpaar 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 undwenn innerhalb der Methoden gemeinsame Objektdatenverandert werden konnen. Wenn fur die Anwendung si-chergestellt ist, dass eine Methode jeweils nur von einemThread zugegriffen wird, kann eine Synchronisation zurErhohung der Effizienz vermieden werden.

• Die Synchronisation sollte auf die kritischen Bereichebeschrankt werden, um so die Zeit der Sperrung vonObjekten zu reduzieren. Anstelle von synchronized-Methoden sollten bevorzugt synchronized-Blocke ver-wendet werden.

• Die Mutexvariable eines Objektes sollte nicht zur Synch-ronisation nicht zusammenhangender kritischer Berei-che verwendet werden, da dies zu unnotigen Sequentia-lisierungen fuhren kann.

• Einige Java-Klassen sind bereits intern synchronisiert;Beispiele sind Hashtable, Vector und StringBuffer.Zusatzliche Synchronisation ist fur Objekte dieser Klas-sen also uberflussig.

• Ist fur ein Objekt Synchronisation erforderlich, solltendie Daten in private oder protected Feldern abge-legt werden, damit kein unsynchronisierter Zugriff vonaußen moglich ist; alle zugreifenden Methoden mussensynchronized sein.

• Greifen Threads eines Programms in unterschiedlicherReihenfolge auf Objekte zu, konnen Deadlocks durchVerwendung der gleichen Sperr-Reihenfolge verhindertwerden.

Page 108: Multicore Parallele Programmierung Kng617

100 5 Java-Threads

Die Realisierung von synchronized-Blocken mit Hilfeder impliziten Mutexvariablen, die jedem Objekt zugeord-net sind, funktioniert fur alle Methoden, die bzgl. eines Ob-jektes aktiviert werden. Statische Methoden einer Klassewerden jedoch nicht bzgl. eines speziellen Objektes akti-viert und eine implizite Objekt-Mutexvariable existiert da-her nicht. Nichtsdestotrotz konnen auch statische Metho-den als synchronized deklariert werden. Die Synchroni-sation erfolgt dann uber die Mutexvariable des zugehori-gen Klassenobjektes der Klasse java.lang.Class, das furdie Klasse, in der die statische Methode deklariert wird,automatisch erzeugt wird. Statische und nicht-statischesynchronized Methoden einer Klasse verwenden also un-terschiedliche Mutexvariablen fur die Synchronisation.

Eine statische synchronized-Methode kann sowohl dieMutexvariable der Klasse als auch die Mutexvariable einesObjektes der Klasse sperren, indem sie eine nicht-statischeMethode bzgl. eines Objektes der Klasse aufruft oder einObjekt der Klasse zur Synchronisation nutzt. Dies wird inAbb. 5.7 anhand der Klasse MyStatic illustriert.

Eine nicht-statische synchronizedMethode kann durchden Aufruf einer statischen synchronized Methode eben-falls neben der Objekt-Mutexvariablen auch die Klassen-Mutexvariable sperren. Fur eine Klasse Cl kann die Synch-ronisation bzgl. der Klassen-Mutexvariablen auch direktdurch

synchronized (Cl.class) { /* Rumpf*/ }

erfolgen.

Page 109: Multicore Parallele Programmierung Kng617

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) {// zusatzliche Verwendung der Objekt-Sperre

}}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 Threadauf das Eintreten einer anwendungsspezifischen Bedingungwartet. Sobald die Bedingung erfullt ist, fuhrt der Threadeine festgelegte Aktion aus. So lange die Bedingung nochnicht erfullt ist, wartet der Thread darauf, dass ein andererThread durch entsprechende Berechnungen das Eintretender Bedingung herbeifuhrt. In Pthreads konnten fur solcheSituationen Bedingungsvariablen eingesetzt werden. Javastellt uber die Methoden wait() und notify(), die in dervordefinierten Klasse Object deklariert sind, einen ahnli-chen Mechanismus zur Verfugung. Diese Methoden stehensomit fur jedes Objekt zur Verfugung, da jedes Objekt di-rekt oder indirekt von der Klasse Object abgeleitet ist.Beide Methoden durfen nur innerhalb eines synchronized-Blocks oder einer synchronized-Methode aufgerufen wer-den. Das typische Verwendungsmuster fur wait() ist:

Page 110: Multicore Parallele Programmierung Kng617

102 5 Java-Threads

synchronized (lockObject) {while (!Bedingung) { lockObject.wait(); }Aktion;

}

Der Aufruf von wait() blockiert den aufrufenden Threadso lange, bis er von einem anderen Thread per notify()aufgeweckt wird. Die Blockierung bewirkt auch die Frei-gabe der impliziten Mutexvariable des Objektes, bzgl. derder Thread synchronisiert. Damit kann diese Mutexvaria-ble von einem anderen Thread gesperrt werden. Ein Auf-ruf von notify() weckt einen bezuglich des zugehorigenObjektes blockierten Thread auf. Der aufgeweckte Threadwird ausfuhrungsbereit und versucht, die Kontrolle uber dieimplizite Mutexvariable des Objektes wieder zu erhalten.Erst wenn ihm dies gelingt, fuhrt er die nach Eintreten derBedingung durchzufuhrende Aktion aus. Wenn dies nichtgelingt, blockiert der Thread bzgl. der Mutexvariablen, bisdiese von dem Thread, der sie gesperrt hat, wieder freige-geben wird.

Die Arbeitsweise von wait() und notify() ahnelt derArbeitsweise von Pthread-Bedingungsvariablen und denOperationen pthread cond wait() und pthread cond -signal(), vgl. Seite 70. Die Implementierung von wait()und notify() erfolgt mit Hilfe einer impliziten Warteliste,in der fur jedes Objekt eine Menge von wartenden Thre-ads gehalten wird. Die Warteliste enthalt jeweils die Thre-ads, die zum aktuellen Zeitpunkt durch Aufruf von wait()bezuglich dieses Objektes blockiert wurden. Nicht in derWarteliste 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 derJava-Sprachspezifikation nicht festgelegt. Mit Hilfe der Me-

Page 111: Multicore Parallele Programmierung Kng617

5.3 Signalmechanismus in Java 103

thode notifyAll() werden alle in der Warteliste abgeleg-ten Threads aufgeweckt und ausfuhrungsbereit; die analogePthreads-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 Große verwendet werden, in denProduzenten-Threads Datenobjekte ablegen und aus demKonsumenten-Threads Daten zur Weiterverarbeitung ent-nehmen konnen.

Abbildung 5.8 zeigt eine threadsichere Implementierungeines Puffermechanismus mit Hilfe des Java-Signalmecha-nismus, vgl. auch [40]. Beim Erzeugen eines Objektes vomTyp BoundedBufferSignalwird ein Feld array vorgegebe-ner Große capacity erzeugt, das als Puffer dient. ZentraleMethoden 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 einemder drei Zustande voll, teilweise voll und leer sein, siehe5.9 fur eine Veranschaulichung der Ubergange zwischen denZustanden. Die Zustande sind durch folgende Bedingungencharakterisiert:

Zustand Bedingung put takemoglich moglich

voll size == capacity nein jateilweise voll 0 < size < capacity ja ja

leer size == 0 ja nein

Page 112: Multicore Parallele Programmierung Kng617

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 mitdem Java-Signalmechanismus.

Page 113: Multicore Parallele Programmierung Kng617

5.3 Signalmechanismus in Java 105

voll vollteilweise leer

take take

put put

Abbildung 5.9. Veranschaulichung der Zustande eines threadsi-cheren Puffermechanismus.

Bei der Ausfuhrung einer put()-Operation durch einenProduzenten-Thread wird dieser mittels wait() blockiert,wenn der Puffer voll ist. Wird eine put()-Operation aufeinem vorher leeren Puffer ausgefuhrt, werden nach Ablagedes Datenobjektes alle wartenden (Konsumenten)-Threadsmit notifyAll() aufgeweckt.

Bei der Ausfuhrung einer take()-Operation durch einenKonsumenten-Thread wird dieser mit wait() blockiert,wenn der Puffer leer ist. Wird eine take()-Operation aufeinem vorher vollen Puffer ausgefuhrt, werden nach Ent-nahme des Datenobjektes alle wartenden (Produzenten)-Threads mit notifyAll() aufgeweckt. Die Implementie-rung von put() und take() stellt sicher, dass ein Objektder Klasse BoundedPufferSignal von einer beliebigen An-zahl von Threads zugegriffen werden kann, ohne dass zeit-kritische Ablaufe entstehen.

Weitere Methoden

Die Klasse Object stellt zwei Varianten von wait() zurVerfugung, die die Angabe einer maximalen Wartezeit inMillisekunden bzw. zusatzlichen Nanosekunden erlauben:

void wait (long msecs)void wait (long msecs, int nanos)

Beide Varianten haben den gleichen Effekt wie wait() ohneParameter mit dem Unterschied, dass die Blockierung des

Page 114: Multicore Parallele Programmierung Kng617

106 5 Java-Threads

Threads automatisch aufgehoben wird, sobald das als Pa-rameter angegebene Zeitintervall msecs abgelaufen ist. Dadiese beiden Varianten ebenfalls in einem synchronized-Block oder einer synchronized-Methode stehen mussen,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. resultierendeWartezeit besteht keine Garantie dafur, dass der vorherblockierte Thread nach Ablauf des angegebenen Zeitinter-valls tatsachlich wieder zur Ausfuhrung kommt. Es kannauch keine Obergrenze fur die zusatzliche Wartezeit ange-geben werden. Es gibt fur den aufgeweckten Thread auchkeine Moglichkeit festzustellen, ob er durch Ablauf des an-gegebenen Zeitintervalls oder durch Aufruf von notify()durch einen anderen Thread aufgeweckt wurde. Die Auf-rufe wait(0) bzw. wait(0,0) sind aquivalent zum Aufrufwait() ohne Parameter.

Ein durch einen Aufruf von wait(), sleep() oder join()blockierter Thread kann auch dadurch wieder aufgewecktwerden, dass er von einem anderen Thread unterbrochenwird. Dazu steht die Methode

void interrupt()

der Klasse Thread zur Verfugung. Durch Aufruf dieserMethode wird der blockierte Thread mit der AusnahmeInterruptedException aufgeweckt, die gemaß der ubli-chen Regeln fur die Ausnahmebehandlung verarbeitet wer-den kann. Dies wird von den Methoden put() und take()in Abb. 5.8 berucksichtigt. Auf einen nicht blockiertenThread t hat der Aufruf von t.interrupt() den Effekt,dass das Interrupt-Flag des Threads t auf true gesetzt

Page 115: Multicore Parallele Programmierung Kng617

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() odersleep() durch diesen Thread direkt die Ausnahme Inter-ruptedException ausgelost. Ein Thread kann seinen eige-nen Interrupt-Status durch Aufruf der statischen Methode

static boolean interrupted()

der Klasse Thread uberprufen. Der Interrupt-Status einesbeliebigen Threads kann durch Aufruf der nicht-statischenMethode

boolean isInterrupted()

fur das entsprechende Objekt der Klasse Thread abgefragtwerden. Es ist zu beachten, dass das Unterbrechen einesThreads mit interrupt() nicht unbedingt seine Terminie-rung nach sich zieht, obwohl dies fur die meisten Anwendun-gen der Normalfall ist. Ein vorher nicht blockierter Threadkann 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 ausfuhrenden Threadfur das angegebene Zeitintervall. Im Unterschied zu wait()muss sleep() aber nicht in einem synchronized-Blockstehen. Ein Aufruf von sleep() hat auch keinen Einflussauf eine entl. vom ausfuhrenden Thread gesperrte implizi-te Mutexvariable eines Objektes. Wenn sleep() in einemsynchronized Block steht, fuhrt der Aufruf von sleep()also nicht zur impliziten Freigabe der Mutexvariable deszugehorigen Objektes, und die Mutexvariable bleibt in die-sem Fall wahrend der Wartezeit des Threads gesperrt. NachAblauf des Zeitintervalls muss der ausfuhrende Thread, im

Page 116: Multicore Parallele Programmierung Kng617

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 ausfuhrungsbereit.

Die Methoden wait() und notify() sind nicht-statischeMethoden der Klasse Object und konnen daher durchstatische nicht direkt aufgerufen werden, da es fur stati-sche Methoden keine zugehorige Objektreferenz gibt. Umwait() bzw. notify() in statischen Methoden verwendenzu konnen, muss ein zusatzliches Objekt erzeugt werden,bezuglich dem die Synchronisation in Form von wait()und notify() durchgefuhrt werden kann. Dies kann einbeliebiges Objekt der Klasse Object sein, aber auch dasClass-Objekt der Klasse, in der die zu synchronisierendenstatischen Methoden enthalten sind. Dies ist in Abbildung5.10 am Beispiel einer Klasse mit zwei statischen Methodenillustriert.

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().

Page 117: Multicore Parallele Programmierung Kng617

5.4 Erweiterte Synchronisationsmuster 109

5.4 Erweiterte Synchronisationsmuster

Die vorgestellten Synchronisationsmechanismen fur Java-Threads konnen dazu verwendet werden, komplexere Syn-chronisationsmuster zu realisieren, die haufig in parallelenAnwendungsprogrammen eine Rolle spielen. Dies wird amBeispiel eines Semaphor-Mechanismus (vgl. S. 50) gezeigt.

Ein Semaphor-Mechanismus kann mit Hilfe von wait()und notify() in Java realisiert werden. Abb. 5.11 zeigteine einfache Realisierung, vgl. auch [40, 52]. Die Metho-de acquire() wartet (wenn notwendig), bis der interneZahler des Semaphor mindestens den Wert 1 angenommenhat. Sobald dies der Fall ist, wird der Zahler dekremen-tiert. Die Methode release() inkrementiert den Zahlerund weckt mit notify() einen wartenden Thread auf, derin acquire() durch den Aufruf von wait() blockiert wur-de. Einen wartenden Thread kann es nur geben, wenn derZahler vor dem Inkrementieren den Wert 0 hatte, dennnur dann wird ein Thread in acquire() blockiert. Da derZahler nur um 1 inkrementiert wurde, reicht es aus, einenwartenden Thread aufzuwecken. Die Alternative ware derEinsatz von notifyAll(), wodurch alle wartenden Thre-ads aufgeweckt wurden. Von diesen konnte aber nur einerden Zahler dekrementieren. Da danach der Zahler wiederden Wert 0 hat, wurden alle anderen Threads durch denAufruf von wait wieder blockiert.

Der in Abb. 5.11 beschriebene Semaphor-Mechanismuskann fur die Synchronisation eines Produzenten-Konsu-menten-Verhaltnisses zwischen Threads verwendet werden.Ein ahnlicher Mechanismus wurde in Abb. 5.8 direkt mitwait() und notify() realisiert. Abb. 5.13 zeigt eine al-ternative Realisierung mit Semaphoren, vgl. auch [40]. DerProduzent legt die von ihm erzeugten Objekte in einemPuffer fester Große ab, der Konsument entnimmt Objekte

Page 118: Multicore Parallele Programmierung Kng617

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 Produzentkann nur Objekte im Puffer ablegen, wenn dieser nicht vollist, der Konsument kann nur Objekte entnehmen, wennder Puffer nicht leer ist. Die eigentliche Pufferverwaltungwird durch eine separate Klasse BufferArray realisiert, dieMethoden insert() bzw. extract() zum Einfugen bzw.Entnehmen von Objekten zur Verfugung stellt, vgl. Abb.

Page 119: Multicore Parallele Programmierung Kng617

5.4 Erweiterte Synchronisationsmuster 111

5.12. Beide Methoden sind synchronized, so dass mehre-re Threads konkurrierend auf Objekte der Klasse zugreifenkonnen. Ein Mechanismus zur Kontrolle eines eventuellenPufferuberlaufs ist nicht enthalten.

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. Entnahmeeines Objektes im Puffer zur Verfugung. Zur Kontrolledes Puffers werden zwei Semaphore putPermits() undtakePermits() verwendet, die zu jedem Zeitpunkt dieerlaubte Anzahl von Ablagen (Produzent) bzw. Entnah-men (Konsument) angeben; putPermits() wird mit derPuffergroße, takePermits() mit 0 initialisiert. Beim Ab-

Page 120: Multicore Parallele Programmierung Kng617

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 SemaphorputPermits() mit acquire() dekrementiert; bei vollemPuffer 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-

Page 121: Multicore Parallele Programmierung Kng617

5.5 Thread-Scheduling in Java 113

nahme eines Objektes mittels take() arbeitet analog mitvertauschter Rolle der Semaphoren.

Im Vergleich zur Realisierung aus Abb. 5.8 verwendetdie 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 desSynchronisationsaufwands fuhren: Bei der Implementierungaus Abb. 5.8 werden in put() bzw. take() alle warten-den Threads aufgeweckt. Von diesen kann jedoch nur einerweiterarbeiten, indem er ein abgelegtes Objekt entnimmtoder 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 nurein Thread aufgeweckt, der darauf wartet, dass der Puffernicht mehr leer bzw. nicht mehr voll ist.

5.5 Thread-Scheduling in Java

Ein Java-Programm besteht typischerweise aus mehrerenThreads, die auf einem oder mehreren Prozessoren aus-gefuhrt werden. Die ausfuhrungsbereiten Threads konkur-rieren dabei um die Ausfuhrung auf einem freiwerdendenProzessor. Die jeweilige Zuordnung von Threads an Prozes-soren wird vom Scheduler der JVM durchgefuhrt. Der Pro-grammierer kann die Zuordnung von Threads an Prozesso-ren dadurch beeinflussen, dass er Threads Prioritaten zu-ordnet. Die minimalen, maximalen und Default-Prioritatenvon Java-Threads sind in statischen Konstanten der KlasseThread festgelegt:

public static final int MIN PRIORITY // Default 1public static final int MAX PRIORITY // Default 10public static final int NORM PRIORITY // Default 5

Page 122: Multicore Parallele Programmierung Kng617

114 5 Java-Threads

Dabei entspricht ein großer Prioritatswert einer hohen Prio-ritat. Der die main()-Methode einer Klasse ausfuhrendeHauptthread hat per Default die Prioritat Thread.NORM -PRIORITY. Ein neu erzeugter Thread hat per Default diegleiche Prioritat wie der erzeugende Thread. Die aktuellePrioritat eines Threads kann mit Hilfe der Methoden

public int getPriority()public int setPriority(int prio)

abgefragt bzw. dynamisch geandert werden.Gibt es mehr ausfuhrungsbereite Threads als Prozesso-

ren, bringt der Scheduler vorzugsweise Threads mit einerhoheren Prioritat zur Ausfuhrung. Der exakte Mechanis-mus zur Auswahl der auszufuhrenden Threads kann von derspeziellen Implementierung der JVM abhangen. Die Pro-grammiersprache Java legt keinen genauen Mechanismusfur das Scheduling fest, um die Flexibilitat der Realisie-rung der JVM auf verschiedenen Plattformen und Betriebs-systemen nicht zu beeintrachtigen. Der Scheduler kann im-mer den Thread mit der hochsten Prioritat zur Ausfuhrungbringen, er kann aber auch einen Alterungsmechanismus in-tegrieren, der sicherstellt, dass auch Threads mit geringe-rer Prioritat ab und zu zur Ausfuhrung kommen. Da dasgenaue Scheduling von Threads unterschiedlicher Prioritatnicht festgelegt ist, konnen Prioritaten nicht dazu verwen-det werden, Synchronisationsmechanismen zu ersetzen.

Bei Verwendung von Threads mit unterschiedlichen Prio-ritaten kann das Problem der Prioritatsinversion auftre-ten: Eine Prioritatsinversion tritt auf, wenn ein Threadhoher Prioritat blockiert und auf einen Thread niedrigerPrioritat wartet, weil dieser z.B. eine Mutexvariable ge-sperrt hat. Der Thread niedriger Prioritat kann aber voneinem Thread mittlerer Prioritat am Weiterarbeiten und ander Freigabe der Mutexvariable gehindert werden mit dem

Page 123: Multicore Parallele Programmierung Kng617

5.6 Paket java.util.concurrent 115

Effekt, dass der Thread hoher Prioritat moglicherweise lan-ge Zeit blockiert. Das Problem der Prioritatsinversion kanndurch Verwendung von Prioritatsvererbung gelost werden:wenn ein Thread hoher Prioritat blockiert, wird die Prio-ritat des Threads, der das kritische Objekt zur Zeit kon-trolliert auf die Prioritat des Threads hoher Prioritat an-gehoben. Damit kann kein Thread mittlerer Prioritat denThread hoher Prioritat vom Weiterarbeiten abhalten. VieleJVM setzen daher diese Methode ein; dies ist jedoch nichtvom 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.concurrentzusatzliche Synchronisationsmechanismen zur Verfugung,die auf den bisher besprochenen Mechanismen, also syn-chronized-Blocke, wait() und notify(), aufbauen. Diezusatzlichen Mechanismen stellen abstraktere und flexible-re Synchronisationsoperationen zur Verfugung. Diese bein-halten u.a. atomare Variablen, Sperrvariablen, Barrier-Syn-chronisation, Bedingungsvariablen und Semaphore sowieverschiedene threadsichere Datenstrukturen. Die zusatzli-chen Klassen sind ahnlich zu den in [40] besprochenen Klas-sen. Wir geben im Folgenden einen kurzen Uberblick undverweisen fur eine detailliertere Behandlung auf [25].

Semaphor-Mechanismus

Die Klasse Semaphore stellt einen Semaphor-Mechanismusahnlich zu Abb. 5.11 zur Verfugung. Intern enthalt dieKlasse einen Zahler, der die Anzahl der Zugriffserlaubnissezahlt. Die wichtigsten Methoden der Klasse sind:

Page 124: Multicore Parallele Programmierung Kng617

116 5 Java-Threads

void acquire();void release();boolean tryAcquire()boolean tryAcquire(int permits, long timeout,

TimeUnit unit)

Die Methode acquire() erfragt eine Zugriffserlaubnis undblockiert, falls keine vorhanden ist. Ist eine Zugriffserlaub-nis vorhanden, wird die Anzahl der vorhandenen Zugriffs-erlaubnisse dekrementiert und die Kontrolle wird direktwieder dem aufrufenden Thread ubergeben. Die Methoderelease() fugt eine Zugriffserlaubnis zum Semaphor hin-zu. Wartet zu diesem Zeitpunkt ein anderer Thread aufeine Zugriffserlaubnis, wird er aufgeweckt. Die MethodetryAcquire() versucht, eine Zugriffserlaubnis zu erhalten.Ist dies erfolgreich, wird true zuruckgeliefert. Ist dies nichterfolgreich, wird false zuruckgeliefert; im Unterschied zuacquire() erfolgt also keine Blockierung des ausfuhrendenThreads. Die Methode tryAcquire() mit Parametern er-laubt die zusatzliche Angabe einer Anzahl von Zugriffser-laubnissen (permits) und einer Wartezeit (timeout) mit ei-ner Zeiteinheit (unit). Sind nicht genugend Zugriffserlaub-nisse verfugbar, wird der ausfuhrende Thread blockiert, biseine der folgenden Bedingungen eintritt:

• die Anzahl der angefragten Zugriffserlaubnisse wird ver-fugbar, indem andere Threads release() ausfuhren(Ruckgabewert true);

• die angegebene Wartezeit ist abgelaufen (Ruckgabewertfalse);

Barrier-Synchronisation

Die Klasse CyclicBarrier aus java.util.concurrent lie-fert einen Barrier-Synchronisationsmechanismus,wobei sich

Page 125: Multicore Parallele Programmierung Kng617

5.6 Paket java.util.concurrent 117

die Bezeichnung Cyclic darauf bezieht, dass ein Objekt derKlasse wiederverwendet werden kann, wenn alle Threadsdie 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 dieBarrier passieren mussen sowie die Angabe einer Aktionaction, die ausgefuhrt wird, sobald alle Threads die Bar-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 Aufrufder Methode reset() wird ein Barrierobjekt wieder in denursprunglichen Zustand zuruckgesetzt.

Sperrmechanismus

Das Paket java.util.concurrent.locks enthalt Interfa-ces und Klassen fur Sperren und das Warten auf das Ein-treten von Bedingungen.

Das Interface Lock definiert uber synchronized-Blockeund -Methoden hinausgehende Sperrmechanismen, die nichtnur auf eine Synchronisation bzgl. der impliziten Mutexva-riablen der jeweiligen Objekte beschrankt sind. Die wich-tigsten definierten Methoden sind:

void lock()boolean tryLock()boolean tryLock(long time, TimeUnit unit)void unlock()

Die Methode lock() fuhrt einen Sperrversuch durch. Istdie Sperre bereits von einem anderen Thread gesetzt, wirdder ausfuhrende Thread blockiert, bis der andere Thread

Page 126: Multicore Parallele Programmierung Kng617

118 5 Java-Threads

ihn mit unlock() wieder aufweckt. Ist die Sperre nicht ge-setzt, wird der ausfuhrende Thread Eigentumer der Sper-re. Die Methode tryLock() fuhrt ebenfalls einen Sperrver-such durch. Bei Erfolg wird true als Ruckgabewert zuruck-geliefert. Bei Misserfolg wird false zuruckgeliefert, derausfuhrende Thread wird aber nicht blockiert. Die MethodetryLock() mit Parametern erlaubt die zusatzliche Anga-be einer Wartezeit analog zu tryAcquire(). Die Methodeunlock() gibt eine vorher gesetzte Sperre wieder frei. Da-bei wird ein auf die Sperre wartender Thread aufgeweckt.

Eine Realisierung des Interface Lock wird durch dieKlasse ReentrantLock zur Verfugung gestellt. Der Kon-struktor der Klasse erlaubt die Angabe eines optionalenFairness-Parameters:

ReentrantLock()ReentrantLock(boolean fairness)

Wird dieser auf true gesetzt, erhalt im Zweifelsfall der amlangsten wartende Thread Zugriff auf das Sperrobjekt. Oh-ne Verwendung des Fairness-Parameters kann von keinerspeziellen Zugriffsreihenfolge ausgegangen werden. Die Ver-wendung des Fairness-Parameters kann zu einem erhohtenVerwaltungsaufwand und dadurch verringertem Durchsatzfuhren. Eine typische Benutzung der Klasse ReentrantLockist in Abb. 5.14 skizziert.

Signalmechanismus

Das Interface Condition aus java.util.concurrent.lockspezifiziert einen Signalmechanismus mit Bedingungsvaria-blen, so dass ein Thread auf das Eintreten einer Bedin-gung warten kann, deren Eintreten ihm durch ein Signaleines anderen Threads mitgeteilt wird, wie dies auch inPthreads durchgefuhrt wird (vgl. S. 70). Eine Bedingungs-variable wird immer an eine Sperrvariable (vgl. Interface

Page 127: Multicore Parallele Programmierung Kng617

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 vonReentrantLock-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 zuruckgelieferte Bedingungsvariable istfest an die Sperrvariable gebunden, bzgl. der die MethodenewCondition() aufgerufen wird. Auf eine Bedingungsva-riable konnen die folgenden Methoden angewendet werden:

void await()void await(long time, TimeUnit unit)void signal()void signalAll()

Die Methode await() blockiert den ausfuhrenden Thread,bis er von einem anderen Thread wieder mit einem Signalaufgeweckt wird. Gleichzeitig wird die zugehorige Sperrva-riable atomar freigegeben. Vor dem Aufruf von await()

Page 128: Multicore Parallele Programmierung Kng617

120 5 Java-Threads

muss der ausfuhrende Thread also die zugehorige Sperr-variable erfolgreich gesperrt haben. Nach dem Aufweckendurch 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 Threadnach Ablauf der angegebenen Wartezeit aufgeweckt, auchwenn noch kein Signal eines anderen Threads eingetroffenist.

Mit signal() kann ein Thread einen bzgl. einer Be-dingungsvariable wartenden Thread wieder aufwecken. MitsignalAll()werden alle bzgl. der Bedingungsvariable war-tenden Threads aufgeweckt.

Die Verwendung von Bedingungsvariablen fur die Reali-sierung eines Puffermechanismus ist in Abb. 5.15 illustriert.Die Bedingungsvariablen werden ahnlich wie der Semaphorin Abb. 5.13 verwendet.

Atomare Operationen

Das Paket java.util.concurrent.atomic stellt fur ele-mentare Datentypen atomare Operationen zur Verfugung,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()

enthalt. Die erste Methode setzt den Wert der Variablen aufupdate, falls der Wert vorher expect war, und liefert truebei erfolgreicher Ausfuhrung zuruck. Die Operation erfolgtatomar, d.h. wahrend der Ausfuhrung kann der Threadnicht unterbrochen werden. Die zweite Methode inkremen-tiert den Wert der Variablen atomar um 1 und liefert den

Page 129: Multicore Parallele Programmierung Kng617

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 Hilfevon Bedingungsvariablen.

Page 130: Multicore Parallele Programmierung Kng617

122 5 Java-Threads

fruheren Wert der Variablen als Ergebnis zuruck. Die Klas-se stellt eine Vielzahl ahnlicher Methoden zur Verfugung.

Taskbasierte Ausfuhrung von Programmen

Das Paket java.util.concurrent stellt auch einen Me-chanismus fur eine taskbasierte Formulierung von Program-men bereit. Eine Task ist dabei eine durchzufuhrende Be-rechnungsfolge des Programms, die von einem beliebigenThread ausgefuhrt werden kann. Eine Abarbeitung vonTasks wird durch das Interface Executor unterstutzt:

public interface Executor {void execute (Runnable command);

}Dabei beschreibt command die auszufuhrende Task, derdurch den Aufruf von execute() zur Ausfuhrung gebrachtwird. Fur Multicore-Prozessoren stehen dabei ublicherweisemehrere Threads zur Ausfuhrung von Tasks zur Verfugung.Diese konnen in einem Thread-Pool zusammengefasst wer-den, wobei jeder Thread eine beliebige Task ausfuhrenkann. Im Vergleich zu einer Ausfuhrung jeder Task in ei-nem eigenen Thread fuhrt der Einsatz von Thread-Poolstypischerweise zu einem geringeren Verwaltungsaufwand,insbesondere wenn die Tasks wenige Berechnungen umfas-sen. Zur Organisation von Thread-Pools kann die KlasseExecutors eingesetzt werden, die Methoden zur Erzeugungund Verwaltung von Thread-Pools bereitstellt. Die wich-tigsten sind

static ExecutorService newFixedThreadPool(int n)

static ExecutorService newCachedThreadPool()

static ExecutorService newSingleThreadExecutor()

Page 131: Multicore Parallele Programmierung Kng617

5.6 Paket java.util.concurrent 123

Die erste Methode erzeugt einen Thread-Pool, der neueThreads bei Einfugen von Tasks startet, bis die angegebe-ne maximale Anzahl n von Threads erreicht ist. Die zweiteMethode erzeugt einen Thread-Pool, bei dem die Anzahlder Threads dynamisch an die Anzahl der Tasks angepasstwird, wobei Threads wieder terminiert werden, wenn sie fureine bestimmte Zeit (60 Sekunden) nicht genutzt werden.Die letzte Methode erzeugt einen einzelnen Thread, der eineMenge von Tasks abarbeitet.

Zur Unterstutzung der Abarbeitung taskbasierter An-wendungen definiert das von Executor abgeleitete Inter-face ExecutorService u.a. Methoden zur Terminierungvon Thread-Pools. Die wichtigsten dieser Methoden sind:

void shutdown();List<Runnable> shutdownNow();

Die Methode shutdown() bewirkt, dass der Thread-Poolkeine weiteren Tasks mehr annimmt; die bereits enthal-tenen Tasks werden aber noch ausgefuhrt. Die Metho-de shutdownNow() stoppt zusatzlich alle im Moment aus-gefuhrten Tasks; wartende Tasks werden nicht mehr aus-gefuhrt. Die Liste der wartenden Tasks wird als Ruck-gabewert zuruckgeliefert. Die Klasse ThreadPoolExecutorstellt eine Realisierung des Interfaces ExecutorService zurVerfugung.

Abb. 5.16 illustriert die Verwendung eines Thread-Poolsam Beispiel eines Webservers, der uber einen ServerSocketauf Verbindungsanfragen von Clients wartet und diese alsTasks mit execute() von den Threads eines Thread-Poolsbearbeiten laßt. Jede abzuarbeitende Task wird als Ob-jekt vom Typ Runnable erzeugt und spezifiziert die durch-zufuhrende Berechnung handleRequest() als run()-Me-thode. Die Große des Thread-Pools ist auf 10 Threads be-grenzt.

Page 132: Multicore Parallele Programmierung Kng617

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.

Page 133: Multicore Parallele Programmierung Kng617

6

OpenMP

OpenMP ist eine Spezifikation von Ubersetzerdirektiven,Bibliotheksfunktionen und Umgebungsvariablen, die von ei-ner Gruppe von Soft- und Hardwareherstellern mit demZiel entworfen wurde, einen einheitlichen Standard fur dieProgrammierung von Parallelrechnern mit gemeinsamemAdressraum zur Verfugung zu stellen [53]. Unterstutzt wer-den Schnittstellen fur C, C++ und FORTRAN. OpenMPerweitert diese sequentiellen Sprachen um Konstrukte zurSPMD-Programmierung, zur Aufteilung von Arbeit, zurSynchronisation und zur Deklaration von gemeinsamen(shared) und privaten (private) Variablen. Die Auswahlder Konstrukte ist auf den Anwendungsbereich des wissen-schaftlichen Rechnens ausgerichtet. Es konnen aber auchandere Anwendungen in OpenMP realisiert werden.

6.1 Programmiermodell

Das Programmiermodell von OpenMP basiert auf parallelarbeitenden Threads, die nach einem fork-join-Prinzip

Page 134: Multicore Parallele Programmierung Kng617

126 6 OpenMP

erzeugt und beendet werden. Die Abarbeitung eines mitHilfe von OpenMP formulierten Programms beginnt mitder Ausfuhrung eines Master-Threads, der das Programmsequentiell ausfuhrt, bis das erste parallel-Konstrukt auf-tritt. Bei Auftreten dieses Konstrukts, das weiter untennaher beschrieben wird, erzeugt der Master-Thread einTeam von Threads und wird zum Master des Teams (fork).Alle Threads des Teams, zu dem auch der Master selbergehort, fuhren das auf das parallel-Konstrukt folgendeProgrammstuck parallel zueinander aus, indem entwederalle Threads des Teams den gleichen Programmtext mitevtl. unterschiedlichen privaten Variablen im SPMD-Stilabarbeiten oder indem die Arbeit explizit durch geeigne-te Konstrukte auf die Threads verteilt wird. Dabei wirdein gemeinsamer Adressraum fur das Gesamtprogramm zu-grundegelegt, d.h. wenn ein Mitglied des Teams eine Daten-struktur andert, ist die Anderung nicht nur fur die anderenMitglieder des Teams, sondern auch fur alle anderen Thre-ads des Programms sichtbar. Nach Beendigung der Abar-beitung des parallel auszufuhrenden Programmstuckes wer-den die Threads des Teams synchronisiert und nur der Mas-ter des Teams wird weiter ausgefuhrt; die anderen Threadswerden beendet (join).

Mit den zur Verfugung stehenden Mechanismen zurSteuerung der Parallelitat konnen Programme formuliertwerden, die sowohl sequentiell als auch parallel ausgefuhrtwerden konnen. Dabei ist es jedoch auch moglich, Program-me zu schreiben, die nur bei einer parallelen Ausfuhrungdas gewunschte Ergebnis errechnen. Der Programmierer istdafur verantwortlich, dass die Programme korrekt arbeiten.Dies gilt auch fur die Vermeidung von Konflikten, Dead-locks oder zeitkritischen Ablaufen.

Die meisten Mechanismen zur Steuerung der paralle-len Abarbeitung von Programmteilen werden in OpenMP

Page 135: Multicore Parallele Programmierung Kng617

6.2 Spezifikation der Parallelitat 127

durch Ubersetzer-Direktiven zur Verfugung gestellt, derenSyntax auf den in C und C++ verwendeten #pragma-Direktiven basiert. Zusatzlich stehen Laufzeitfunktionenzur Steuerung des Verhaltens der Direktiven zur Verfugung.Jede Direktive wirkt nur auf die der Direktive direkt folgen-de Anweisung. Sollen mehrere Anweisungen von der Direk-tive gesteuert werden, so mussen diese zwischen { und }stehen und so in einem Anweisungsblock zusammengefasstsein. Fur OpenMP-Programme muss die Datei <omp.h> ein-gebunden werden.

Der Rest des Kapitels enthalt einen kurzen Uberblickuber OpenMP. Weiterfuhrende Informationen konnen in[13, 53, 59] nachgelesen und uber die OpenMP-Webseite(http://www.openmp.org) erhalten werden.

6.2 Spezifikation der Parallelitat

Die wichtigste Direktive zur Steuerung der Parallelitat istdie parallel-Direktive mit der Syntax:

#pragma omp parallel [Parameter [Parameter] ... ]

Anweisungsblock

Diese Direktive bewirkt, dass der angegebene Anwei-sungsblock parallel ausgefuhrt wird. Wird die Arbeit nichtexplizit verteilt, so fuhren alle Threads die gleichen Be-rechnungen mit evtl. unterschiedlichen privaten Daten imSPMD-Stil aus. Der parallel ausgefuhrte Anweisungsblockwird auch als paralleler Bereich bezeichnet. Zur paralle-len Abarbeitung wird ein Team von Threads erzeugt, dessenMaster der die Direktive ausfuhrende Thread ist. Die ge-naue Anzahl der zu erzeugenden Threads kann uber Lauf-zeitfunktionen oder Umgebungsvariablen beeinflusst wer-den. Nach der Erzeugung des Teams bleibt die Anzahl der

Page 136: Multicore Parallele Programmierung Kng617

128 6 OpenMP

Threads, die den Anweisungsblock ausfuhren, konstant. Furverschiedene parallele Bereiche konnen jedoch verschiedeneThread-Anzahlen verwendet werden.

Ein paralleler Bereich wird von allen Threads des er-zeugten Teams einschließlich des Master-Threads gemein-sam abgearbeitet. Dabei konnen gemeinsame und privateVariablen der beteiligten Threads uber die Parameter derparallel-Direktive definiert werden. Private Variablen derThreads werden durch den private-Parameter der Form

private(list of variables)

spezifiziert, wobei list of variables eine beliebige Listevon bereits deklarierten Programmvariablen ist. Der Effektbesteht darin, dass auf dem Laufzeitstack jedes Threads desTeams eine uninitialisierte Kopie der angegebenen Varia-blen angelegt wird, die nur dieser Thread wahrend seinerAusfuhrung als globale Variable zugreifen und verandernkann. 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 desTeams 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 Variablensind. Die Angabe

default(shared)

bewirkt, dass alle außer den vom private-Parameter expli-zit angegebenen Programmvariablen gemeinsame Variablender Threads des Teams sind. Die Angabe

Page 137: Multicore Parallele Programmierung Kng617

6.2 Spezifikation der Parallelitat 129

default(none)

bewirkt, dass jede in dem parallelen Bereich verwendete Va-riable explizit uber einen shared- oder private-Parameterals gemeinsame oder private Variable gekennzeichnet seinmuss.

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 Variableder den parallelen Bereich ausfuhrenden Threads spezifi-ziert. Die restlichen Variablen iam, np und mypoints sindprivate Variablen. Zu Beginn der Ausfuhrung des paralle-len Bereiches bestimmt jeder beteiligte Thread durch denAufruf der Funktion np = omp get num threads() die Ge-samtanzahl der Threads des Teams. Durch den Aufruf derFunktion omp get thread num() erhalt jeder Thread desTeams eine Nummer zuruck, die als Thread-Name dientund im Beispiel in iam gespeichert wird. Der Master-Threadhat 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 Eintrage des Feldes x verarbeitet werden.Beim Aufruf von compute subdomain() wird neben demFeldnamen x and dem Thread-Namen iam auch die Anzahlmypoints 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 fuhrt der Master-Thread die dem par-allelen Bereich folgenden Anweisungen alleine aus. Das En-

Page 138: Multicore Parallele Programmierung Kng617

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 mitHilfe einer OpenMP parallel-Direktive.

de eines parallelen Bereiches stellt somit einen implizitenSynchronisationspunkt dar.

Prinzipiell konnen parallele Bereiche geschachtelt wer-den, d.h. in einem parallelen Bereich kann eine weitereparallel-Direktive auftreten. Per Default wird der inne-re parallele Bereich von einem Team ausgefuhrt, dem nurder Thread angehort, der die innere parallel-Direktiveausfuhrt. Dies kann durch Aufruf der Bibliotheksfunktion

void omp set nested(int nested)

mit nested != 0 geandert werden. In diesem Fall kann derdie geschachtelte parallel-Direktive ausfuhrende Thread

Page 139: Multicore Parallele Programmierung Kng617

6.2 Spezifikation der Parallelitat 131

ein Team mit mehr als einem Thread erzeugen. Die genaueAnzahl der in diesem Fall erzeugten Threads ist implemen-tierungsabhangig.

Parallele Schleife

Innerhalb eines parallelen Bereiches konnen die durch-zufuhrenden Berechnungen mit Hilfe von speziellen Direkti-ven zur Verteilung der Arbeit auf die ausfuhrenden Threadsverteilt werden. Die wichtigste Direktive zur Verteilung derArbeit 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 beschrankt, bei de-nen sichergestellt ist, dass die durch den Schleifenrumpfgegebenen Berechnungen der verschiedenen Iterationen un-abhangig voneinander sind und die Gesamtzahl der Itera-tionen beim Betreten der for-Schleife im voraus bestimmtwerden kann. Der Effekt der for-Direktive besteht dar-in, dass die einzelnen Iterationen der Schleife auf die denumgebenden parallelen Bereich ausfuhrenden Threads ver-teilt und unabhangig berechnet werden. Es soll sich al-so um eine parallele Schleife fester Lange handeln. DieVariable i bezeichnet eine Integervariable, die im Rumpfder Schleife nicht verandert werden darf und die inner-halb der Schleife als private Variable des die zugehorigeIteration der for-Schleife ausfuhrenden Threads behan-delt wird. lower bound und upper bound bezeichnen Inte-gerausdrucke, deren Werte durch Ausfuhrung der Schleifenicht geandert werden, op bezeichnet einen Vergleichsope-rator, also op ∈ { <, <=, >, >= }. Der Inkrementierungs-Ausdruck incr expr kann folgende Formen annehmen:

Page 140: Multicore Parallele Programmierung Kng617

132 6 OpenMP

++i, i++, --i, i--, i += incr, i -= incr,i = i + incr, i = incr + i, i = i - incr,

wobei incr ebenfalls ein schleifenunabhangiger Integeraus-druck ist.

Die Aufteilung der Schleifeniterationen auf dieausfuhrenden Threads kann durch den schedule-Parameter gesteuert werden. Folgende Steuerungsmoglich-keiten sind vorgesehen:

• schedule(static, block size). Diese Parameter-Angabe bedeutet, dass eine statische Aufteilung der Ite-rationen auf die Threads verwendet wird, indem dieIterationen in Blocken der Große block size reihum(round-robin) auf die Threads verteilt werden. Ist kei-ne Blockgroße angegeben, so erhalt jeder Thread einenBlock fortlaufender Iterationen ungefahr gleicher Große,d.h. es wird eine blockweise Verteilung verwendet.

• schedule(dynamic, block size). Diese Parameter-Angabe bedeutet, dass eine dynamische Zuteilung vonIterationsblocken an die Threads vorgenommen wird,d.h. nach Abarbeitung der zugewiesenen Iterationenerhalt ein Thread dynamisch einen neuen Block mitblock size Iterationen zugeteilt. Ist keine Blockgroßeangegeben, werden dynamisch einzelne Iterationen zu-geteilt, d.h. es wird die Blockgroße 1 verwendet.

• schedule(guided, block size). Diese Parameter-Angabe bedeutet, dass ein dynamisches Scheduling mitabnehmender Blockgroße verwendet wird. Fur die An-gabe block size = 1 wird jedem Thread, der seine zu-gewiesenen Iterationen beendet hat, dynamisch ein neu-er Block von Iterationen zugewiesen, dessen Große sichaus dem Quotient der noch nicht bearbeiteten Itera-tionen und der Anzahl der Threads ergibt, so dass dieBlockgroße der zugewiesenen Iterationen linear mit der

Page 141: Multicore Parallele Programmierung Kng617

6.2 Spezifikation der Parallelitat 133

Anzahl der ausgefuhrten Iterationen abnimmt. Fur dieAngabe block size = k mit k > 1 nimmt die Block-große exponentiell zu k ab, der letzte Block kann jedocheine kleinere Große haben. Die Angabe block size gibtalso die minimale Blockgroße an, die (bis auf die ebenerwahnte Ausnahme) gewahlt werden kann. Ist keinWert fur block size angegeben, wird als Defaultwert1 verwendet.

• schedule(runtime). Diese Parameter-Angabe bedeu-tet, dass das Scheduling der Threads zur Laufzeit desProgramms festgelegt wird. Dies kann dadurch gesche-hen, dass vor dem Start des Programms die Umgebungs-variable OMP SCHEDULE durch Angabe von Scheduling-Typ und Blockgroße gesetzt wird, also beispielsweise als

setenv OMP SCHEDULE "dynamic, 4"setenv OMP SCHEDULE "guided"

Wird dabei keine Blockgroße angegeben, wird derDefaultwert verwendet. Außer fur das statische Schedu-ling (static) ist dies block size = 1. Wenn die Um-gebungsvariable OMP SCHEDULE nicht gesetzt ist, hangtdas verwendete Scheduling von der Implementierung derOpenMP-Bibliothek ab.

Fehlt die Angabe eines schedule-Parameters bei der for-Direktive, wird ein Default-Schedulingverfahren verwen-det, das von der Implementierung der OpenMP-Bibliothekabhangt. Die einer for-Direktive zugeordnete paralleleSchleife darf nicht durch eine break-Anweisung beendetwerden. Am Ende der parallelen Schleife findet eine im-plizite Synchronisation der beteiligten Threads statt, d.h.die der parallelen Schleife folgenden Anweisungen werdenerst ausgefuhrt, wenn alle beteiligten Threads die paralleleSchleife beendet haben. Diese Synchronisation kann durch

Page 142: Multicore Parallele Programmierung Kng617

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 Berechnungeiner 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 Parameterlisteder for-Direktive vermieden werden.

Abbildung 6.2 zeigt als Beispiel fur die Anwendung einerfor-Direktive eine Programmskizze zur Realisierung einerMatrix-Matrix-Multiplikation zweier Matrizen MA undMB in OpenMP. Der parallele Bereich des Programms be-

Page 143: Multicore Parallele Programmierung Kng617

6.2 Spezifikation der Parallelitat 135

steht aus zwei Phasen, die durch eine implizite Synchronisa-tion voneinander getrennt sind. In der ersten Phase wird dieErgebnismatrix MC mit 0 initialisiert, in der zweiten Pha-se wird die eigentliche Matrix-Multiplikation durchgefuhrt.Die Aufteilung der Berechnung der Ergebnismatrix auf dieauszufuhrenden Threads erfolgt durch ein statisches Sche-duling, wobei jeder Thread einen Block von Zeilen initiali-siert bzw. berechnet. Da jeder Eintrag und damit auch je-de Zeile der Ergebnismatrix gleichen Berechnungsaufwandhat, ist ein solches statisches Scheduling sinnvoll. Bei derBerechnung 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 außere Schleife uber die Eintrageder jeweiligen Zeile lauft und die innere Schleife zur Berech-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 mussen also auch die parallelen Bereicheso geschachtelt werden, dass in jedem parallelen Bereichhochstens eine for-Direktive enthalten ist.

Nichtiterative parallele Bereiche

Eine nichtiterative Verteilung der innerhalb eines parallelenBereiches durchzufuhrenden Berechnung kann durch Ver-wendung einer sections-Direktive erfolgen, deren Syntaxwie folgt definiert ist:

#pragma omp sections [Parameter [Parameter] ... ]

{[#pragma omp section]

Anweisungsblock[#pragma omp section

Anweisungsblock

Page 144: Multicore Parallele Programmierung Kng617

136 6 OpenMP

...]

}

Innerhalb einer sections-Direktive werden durchsection-Direktiven Abschnitte bezeichnet, die unabhangigvoneinander sind und daher parallel zueinander von ver-schiedenen Threads abgearbeitet werden konnen. Jeder Ab-schnitt beginnt mit #pragma omp section und kann einbeliebiger Anweisungsblock sein. Fur den ersten innerhalbder sections-Direktive definierten Anweisungsblock kanndie Angabe der section-Direktive entfallen. Am Ende ei-ner sections-Direktive findet eine implizite Synchronisa-tion statt, die durch die Angabe eines nowait-Parametersvermieden werden kann.

Syntaktische Abkurzungen

Zur Vereinfachung der Schreibweise fuhrt OpenMPAbkurzungen fur parallele Bereiche ein, in denen nur eineeinzelne for- bzw. sections-Direktive enthalten ist. Fureinen parallelen Bereich mit einer einzelnen for-Direktivekann die folgende Abkurzung verwendet werden:

#pragma omp parallel for [Parameter [Parameter] · · · ]

for(i = lower bound; i op upper bound; incr expr) {Schleifenrumpf

}

Dabei sind als Parameter alle fur die parallel- und furdie for-Direktive zugelassenen Parameter erlaubt. Analogkann als Abkurzung fur eine einzelne in einem paralle-len Bereich enthaltene sections-Direktive folgendes Kon-strukt verwendet werden:

Page 145: Multicore Parallele Programmierung Kng617

6.2 Spezifikation der Parallelitat 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 Threadsausgefuhrt. Der Programmierer hat die Moglichkeit, dieseAnzahl 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 dieFunktion außerhalb eines parallelen Bereiches aufgerufenwerden muss. Fur dynamic threads �= 0 wird die dy-namische Anpassung durch das Laufzeitsystem erlaubt,d.h. das Laufzeitsystem kann die Anzahl der Threads,die fur nachfolgende parallele Bereiche verwendet wer-den, an die Systemgegebenheiten anpassen. Wahrend derAusfuhrung desselben parallelen Bereiches wird die Anzahlder ausfuhrenden Threads aber stets konstant gehalten.Fur dynamic threads = 0 wird die dynamische Anpas-sung der Thread-Anzahl ausgeschaltet, d.h. das Laufzeit-system verwendet fur nachfolgende parallele Bereiche diederzeit eingestellte Anzahl von Threads. Welche der beidenVarianten den Default darstellt, hangt von der speziellen

Page 146: Multicore Parallele Programmierung Kng617

138 6 OpenMP

OpenMP-Bibliothek ab. Der Status der Thread-Anpassungkann durch Aufruf der Funktion

int omp get dynamic (void)

abgefragt werden. Der Aufruf liefert 0 zuruck, wenn keinedynamische Anpassung vorgesehen ist. Ansonsten wird einWert �= 0 zuruckgeliefert.

Der Programmierer kann durch Aufruf der Funktion

void omp set num threads (int num threads)

die Anzahl der Threads beeinflussen, die fur die Ausfuhrungnachfolgender paralleler Bereiche verwendet werden. Auchdieser Aufruf muss außerhalb eines parallelen Bereichesstattfinden. Der genaue Effekt des Aufrufes hangt davon ab,ob die automatische Thread-Anpassung durch das Laufzeit-system eingeschaltet ist oder nicht. Wenn die automatischeAnpassung eingeschaltet ist, gibt num threads die maxi-male Anzahl von Threads an, die im Folgenden verwendetwird. Wenn keine automatische Anpassung erlaubt ist, gibtnum threads die tatsachliche Anzahl von Threads an, diefur alle nachfolgenden parallelen Bereiche verwendet wird.

Wie oben dargestellt wurde, erlaubt OpenMP geschach-telte parallele Bereiche. Die Anzahl der zur Abarbeitungverwendeten Threads eines geschachtelten parallelen Berei-ches hangt vom Laufzeitsystem ab, kann jedoch vom Pro-grammierer durch Aufruf der Funktion

void omp set nested (int nested)

beeinflusst werden. Fur nested = 0 wird die Abarbeitungdes inneren parallelen Bereiches sequentialisiert und nurvon einem Thread vorgenommen. Dies ist auch die Default-Einstellung. Fur nested �= 0 wird eine geschachtelte paral-lele Abarbeitung erlaubt, d.h. das Laufzeitsystem kann zur

Page 147: Multicore Parallele Programmierung Kng617

6.3 Koordination von Threads 139

Abarbeitung des inneren parallelen Bereiches zusatzlicheThreads verwenden. Die genaue Abarbeitung hangt auchhier wieder vom Laufzeitsystem ab und kann auch aus derAbarbeitung 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 derRegel von mehreren Threads ausgefuhrt, deren Zugriff aufgemeinsame Variablen koordiniert werden muss. Zur Koor-dination stellt OpenMP mehrere Direktiven zur Verfugung,die innerhalb von parallelen Bereichen verwendet werdenkonnen. Kritische Bereiche, die zu jedem Zeitpunktnur von jeweils einem Thread ausgefuhrt werden sollten,konnen durch die critical-Direktive mit der folgendenSyntax

#pragma omp critical [(name)]Anweisungsblock

realisiert werden. Der optional anzugebende Name namekann dabei zur Identifikation des kritischen Bereiches ver-wendet werden. Der Effekt der critical-Direktive bestehtdarin, dass ein Thread beim Erreichen der Direktive so lan-ge wartet, bis kein anderer Thread den Anweisungsblockdes kritischen Bereiches ausfuhrt. Erst wenn dies erfullt ist,fuhrt der Thread den Anweisungsblock aus.

Die Threads eines Teams konnen mit einer barrier-Direktive

Page 148: Multicore Parallele Programmierung Kng617

140 6 OpenMP

#pragma omp barrier

synchronisiert werden, d.h. erst wenn jeder Thread desTeams diese Direktive erreicht hat, beginnen die Theadsdes Teams die Abarbeitung der nachfolgenden Anweisun-gen.

Durch Angabe der atomic-Direktive konnen bestimm-te Speicherzugriffe als atomare Operationen durchgefuhrtwerden. Die Syntax dieser Direktive ist

#pragma omp atomicZuweisung

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 enthalt, und binop ∈ {+,-, *, /, &, ^, |, <<, >>} einen binaren Operator bezeichnet.Der Effekt besteht darin, dass nach Auswertung des Aus-drucks E die angegebene Aktualisierung von x als atomareOperation erfolgt, d.h. wahrend der Aktualisierung kannkein 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 durcheine critical-Direktive erreicht werden, die vereinfachteForm der atomic-Direktive kann aber evtl. vom Laufzeit-system fur eine effiziente Implementierung ausgenutzt wer-den. Auch ist es moglich mit der atomic-Direktive einzel-ne Feldelemente anzusprechen, wohingegen die critical-Direktive das gesamte Feld schutzen wurde. Beispiele furdie Verwendung von atomaren Operationen sind:

Page 149: Multicore Parallele Programmierung Kng617

6.3 Koordination von Threads 141

extern float a[], *p=a, b; int index[];#pragma omp atomica[index[i]] += b;

#pragma omp atomicp[i] -= 1.0;

Reduktionsoperationen

Um globale Reduktionsoperationen zu ermoglichen,stellt OpenMP fur die parallel-, sections- und for-Direktiven einen reduction-Parameter mit der Syntax

reduction (op: list)

zur Verfugung. Dabei bezeichnet op ∈{+, -, *, /, &, ^, |,&&, ||} den anzuwendenden Reduktionsoperator, list isteine mit Kommata getrennte Liste von Reduktionsvaria-blen, die im umgebenden Kontext als gemeinsame Variabledeklariert sein mussen. Der Effekt des Parameters bestehtdarin, dass bei der Bearbeitung der zugehorigen Direkti-ve fur jede der angegebenen Reduktionsvariablen fur je-den Thread eine private Kopie der Variablen angelegt wird,die entsprechend der angegebenen Reduktionsoperation mitdem neutralen Element dieser Operation initialisiert wird.Den Reduktionsvariablen konnen wahrend der Abarbeitungdes zugehorigen parallelen Bereiches von den verschiedenenThreads Werte zugewiesen werden, die entsprechend derangegebenen Operation op akkumuliert werden. Am Endeder Direktive, fur die der reduction-Parameter angegebenwurde, werden die (gemeinsamen) Reduktionsvariablen ak-tualisiert. Dies geschieht dadurch, dass der ursprunglicheWert der Reduktionsvariablen und die von den Threadswahrend der Abarbeitung der zugehorigen Direktive errech-neten Werte der privaten Kopien entsprechend der Reduk-tionsoperation verknupft werden. Der so errechnete Wert

Page 150: Multicore Parallele Programmierung Kng617

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 werdenvon den Threads, die den parallelen Bereich ausfuhren,fur verschiedene Iterationen der parallelen Schleife durch-gefuhrt. Dabei kann eine Reduktionsoperation auch in ei-nem Funktionsaufruf ausgefuhrt werden, wie dies fur dieBerechnung von y der Fall ist. Nach Beendigung der par-allelen Schleife werden die von den verschiedenen Threadsakkumulierten Werte global in den angegebenen Redukti-onsvariablen akkumuliert.

Sperrmechanismus

Fur den Zugriff auf gemeinsame Variablen stellt OpenMPeinen Sperrmechanismus zur Verfugung, der uber Lauf-zeitfunktionen verwaltet werden kann. Dabei unterschei-det OpenMP zwischen einfachen Sperrvariablen vom Typomp lock t und schachtelbaren Sperrvariablen vom Typomp nest lock t. Der Unterschied besteht darin, dass ei-ne einfache Sperrvariable nur einmal belegt werden kann,wahrend eine schachtelbare Sperrvariable vom gleichenThread mehrfach belegt werden kann. Dazu wird fur die

Page 151: Multicore Parallele Programmierung Kng617

6.3 Koordination von Threads 143

schachtelbare Sperrvariable ein Zahler gehalten, der die An-zahl der Belegungen mitzahlt. Vor Benutzung einer Sperr-variablen muss diese initialisiert werden, wozu die beidenfolgenden Funktionen

void omp init lock (omp lock t *lock)void omp init nest lock (omp nest lock t *lock)

zur Verfugung stehen. Nach der Initialisierung einer Sperr-variablen ist diese nicht belegt. Zur Zerstorung einer initia-lisierten Sperrvariablen stehen die Funktionen

void omp destroy lock (omp lock t *lock)void omp destroy nest lock (omp nest lock t *lock)

zur Verfugung. Nach Initialisierung einer Sperrvariablenkann diese wie ublich zur Koordination des konkurrieren-den Zugriffs auf gemeinsame Daten benutzt werden. ZurBelegung 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 ausfuhrendenThread so lange, bis die angegebene Sperrvariable verfugbarist. Eine einfache Sperrvariable ist verfugbar, wenn sievon keinem anderen Thread belegt ist. Eine schachtelbareSperrvariable ist verfugbar, wenn sie entweder von keinemThread oder vom ausfuhrenden Thread belegt ist. Wenndie angegebene Sperrvariable verfugbar ist, wird sie vomausfuhrenden Thread belegt. Fur schachtelbare Sperrvaria-blen wird der assoziierte Zahler inkrementiert.

Die Belegung einer Sperrvariablen kann durch Aufrufder Funktionen

void omp unset lock (omp lock t *lock)void omp unset nest lock (omp nest lock t *lock)

Page 152: Multicore Parallele Programmierung Kng617

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. Fur eineschachtelbare Sperrvariable dekrementiert der Aufrufomp unset nest lock() den zugeordneten Zahler. Wennder Zahler dadurch den Wert 0 erreicht, wird die Sperrva-riable freigegeben.

Soll beim Versuch der Belegung einer von einem an-deren Thread belegten Sperrvariablen die Blockierung desausfuhrenden Threads vermieden werden, konnen die Funk-tionen

void omp test lock (omp lock t *lock)void omp test nest lock (omp nest lock t *lock)

verwendet werden. Wenn die angegebene Sperrvariableverfugbar ist, wird sie wie bei Aufruf von omp set lock()bzw. omp set nest lock() belegt. Wenn die Sperrva-riable nicht verfugbar ist, wird jedoch der aufrufen-de Thread nicht blockiert. Stattdessen liefern die Funk-tionsaufrufe den Ruckgabewert 0 an den aufrufendenThread zuruck. Bei erfolgreicher Belegung der Sperrva-riablen liefert omp test lock() einen Wert �= 0 zuruck,omp test nest lock() liefert den neuen Wert des zugeord-neten Zahlers zuruck.

Page 153: Multicore Parallele Programmierung Kng617

7

Weitere Ansatze

Bereits jetzt gibt es eine Reihe von erprobten Programmier-umgebungen und -bibliotheken zur Programmierung vonMulticore-Prozessoren, die aus der Programmierung mit ge-meinsamem Adressraum oder dem Multithreading stam-men. Einige wurden in den letzten Kapiteln vorgestellt.Der Einsatz popularer Bibliotheken zur Programmierungeines verteilten Speichers wie z.B. MPI ist durch Portie-rungen ebenfalls bereits moglich. Fur bestehende paralleleProgramme 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 inmoglicherweise veranderten Effekten der parallelen Lauf-zeit. Fur die weit großere Klasse der sequentiellen Pro-gramme ist der Schritt zur parallelen Programmierung mitThreads jedoch schwierig und stellt eine große Umstellungdar [41]. Dies ist auch darin begrundet, dass die Thread-Programmierung mit Sperrmechnismen und anderen Syn-chronisationsformen sowie Folgeproblemen wie Deadlockseinen Programmierstil auf niedriger Ebene darstellt und

Page 154: Multicore Parallele Programmierung Kng617

146 7 Weitere Ansatze

auch mit einer Assembler-Programmierung der Parallelver-arbeitung verglichen wird [63]. Mit solchen Mitteln sindgroße Softwareprojekte schwer zu bewaltigen.

Die Entwicklung zu Multicore-Prozessoren zieht dahereine Forschungswelle nach sich, die sich mit der nebenlaufi-gen und parallelen Programmierung auf hoherer Ebenebeschaftigt. Nicht zu vergessen sind dabei Sprachansatze,die durchaus seit einigen Jahren bestehen und durch dieMulticore-Entwicklung an neuer Bedeutung gewinnen. Ei-nige dieser Sprachen sowie neu entwickelte Sprachen stellenwir in diesem Kapitel vor. Ein breit diskutierter Program-mieransatz ist dabei der Transaktionsmechanismus, der dieneueste Entwicklungsrichtung darstellt und mit der wir dieBeschreibung des derzeitigen Standes der Programmierungvon Multicore-Prozessoren abschließen wollen [57, 1].

7.1 Sprachansatze

Dieser Abschnitt gibt einen kurzen Uberblick uber neuereProgrammiersprachen. Diese Sprachen wurden fur den Be-reich des Hochleistungsrechnens (High Performance Com-puting) entworfen, konnen aber auch zur Programmierungvon Multicore-Systemen eingesetzt werden.

Unified Parallel C

Unified Parallel C (UPC) wurde als Erweiterung von C furden 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 konnen. Jede Variable ist dabei mit einem bestimmten

Page 155: Multicore Parallele Programmierung Kng617

7.1 Sprachansatze 147

Thread assoziiert, kann aber von jedem anderen Thread ge-lesen oder manipuliert werden. Die Zugriffszeit auf die Va-riable ist jedoch fur den assoziierten Thread typischerweisegeringer als fur einen anderen Thread. Zusatzlich konnenfur einen Thread private Daten definiert werden, auf dienur er zugreifen kann.

Parallelitat wird in UPC-Programmen dadurch erreicht,dass beim Programmstart eine festgelegte Anzahl von Thre-ads gestartet wird. Die UPC-Spracherweiterungen von Cbeinhalten ein explizit paralleles Ausfuhrungsmodell, Spei-cherkonsistenzmodelle fur den Zugriff auf gemeinsame Va-riablen, Synchronisationsoperationen und parallele Schlei-fen. Fur eine detailliertere Beschreibung verweisen wirauf [59, 21]. UPC-Compiler sind fur viele Plattformenverfugbar. Freie UPC-Compiler fur Linux sind z.B. derBerkeley UPC-Compiler (upc.nersc.gov) oder der GCCUPC-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 ahnlich zuUPC.

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 unterstutzen sollen. Zu diesen Sprachen gehorenFortress, X10 und Chapel.

Fortress wurde von Sun entwickelt und ist eine anFortran angelehnte neue objektorientierte Sprache, die dieProgrammierung paralleler Systeme durch Verwendung ei-ner mathematischen Notation erleichtern soll [4]. Fortress

Page 156: Multicore Parallele Programmierung Kng617

148 7 Weitere Ansatze

unterstutzt eine parallele Abarbeitung von Programmendurch parallele Schleifen oder die parallele Auswertung vonFunktionsargumenten durch mehrere Threads. Viele Kon-strukte sind dabei implizit parallel, d.h. die erforderlichenThreads werden ohne explizite Steuerung im Programm er-zeugt. So wird z.B. fur jeden Parameter eines Funktions-aufrufs implizit ein separater Thread zur Auswertung ein-gesetzt, ohne dass dies im Programm angegeben werdenmuss. Zusatzlich zu diesen impliziten Threads konnen ex-plizite Threads zur Verarbeitung von Programmteilen ab-gespalten werden. Die Synchronisation dieser Threads er-folgt mit atomic-Ausdrucken; diese stellen sicher, dass derEffekt auf den Speicher erst nach kompletter Abarbeitungdes Ausdrucks atomar sichtbar wird, vgl. auch Abschnitt7.2 uber Transaktionsmechanismen.

X10 wurde von IBM als Erweiterung von Java furden Bereich des Hochleistungsrechnens entwickelt. Ahn-lich zu UPC basiert X10 auf dem PGAS-Speichermodellund erweitert dieses zum GALS-Modell (globally asyn-chronous, locally synchronous) durch Einfuhrung von logi-schen Ausfuhrungsorten (places genannt) [14]. Threads ei-nes Ausfuhrungsortes haben eine lokal synchrone Sicht aufeinen gemeinsamen Adressraum, Threads unterschiedlicherAusfuhrungsorte werden dagegen asynchron zueinanderausgefuhrt. X10 beinhaltet eine Vielzahl von Operationenzur Manipulation von Feldvariablen und Teilen von Feldva-riablen. Mithilfe von Feldverteilungen kann die Aufteilungvon Feldern auf unterschiedliche Ausfuhrungsorte im glo-balen Speicher spezifiziert werden. Fur die Synchronisati-on von Threads stehen atomic-Blocke zur Verfugung, dieeine atomare Ausfuhrung von Anweisungen bewirken. Diekorrekte Verwendung von Sperrmechanismen, z.B. durchsynchronized-Blocke oder -Methoden, wird dadurch demLaufzeitsystem ubertragen.

Page 157: Multicore Parallele Programmierung Kng617

7.1 Sprachansatze 149

Chapel wurde von Cray Inc. als neue parallele Pro-grammiersprache fur Hochleistungsrechnen entworfen [18].Die verwendeten Konstrukte sind teilweise an High-Performance Fortran (HPF) angelehnt. Chapel basiert wieFortress und X10 auf dem Modell eines globalen Adress-raums, in dem Datenstrukturen wie z.B. Felder abgelegtund zugegriffen werden konnen. Die unterstutzte Paralle-litat ist threadbasiert: bei Programmstart gibt es einenHaupt-Thread; durch Verwendung spezieller Sprachkon-strukte (parallele Schleifen) konnen weitere Threads er-zeugt werden, die dann vom Laufzeitsystem verwaltet wer-den. Ein explizites Starten und Beenden von Threads durchden Programmierer entfallt damit. Fur die Koordinationvon Berechnungen auf gemeinsamen Daten stehen Synchro-nisationsvariablen und atomic-Blocke zur Verfugung.

Global Arrays

Zur Unterstutzung der Programmierung von Anwendungendes 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 fur C, C++ und Fortran furunterschiedliche Plattformen zur Verfugung gestellt. DerGA-Ansatz basiert auf einem gemeinsamen Adressraum, indem Felddatenstrukturen (globale Felder) so abgelegt wer-den konnen, dass jedem Prozess ein logischer Block des glo-balen Feldes zugeteilt ist; auf diesen Block kann der Pro-zess schneller zugreifen als auf die anderen Blocke. Die GA-Bibliothek stellt Basisoperationen fur den gemeinsamenAdressraum (put, get, scatter, gather) sowie atomare Ope-rationen und Sperrmechanismen fur den Zugriff auf globaleFelder zur Verfugung. Der Datenaustausch zwischen Pro-zessoren kann uber die globalen Felder, aber auch uber eine

Page 158: Multicore Parallele Programmierung Kng617

150 7 Weitere Ansatze

Message-Passing-Bibliothek wie MPI erfolgen. Ein wichti-ges Anwendungsgebiet des GA-Ansatzes liegt im Bereichchemischer Simulationen.

7.2 Transaktionsspeicher

Fur die Synchronisation von Threads beim Zugriff auf ge-meinsame Daten werden in den meisten Ansatzen Sperr-variablen (Mutexvariablen) und kritische Bereicheverwendet. Dabei wird typischerweise wie folgt vorgegan-gen:

• der Programmierer identifiziert kritische Bereiche imProgramm und schutzt diese explizit mit Sperrvariablen(lock/unlock-Mechanismus);

• der Sperrvariablen-Mechanismus sorgt dafur, dass einkritischer Bereich jeweils nur von einem Thread aus-gefuhrt wird.

Der Ansatz mit Sperrvariablen kann zu einer Sequentia-lisierung der Abarbeitung von kritischen Bereichen fuhrenwas je nach Anwendung die Skalierbarkeit erheblich beein-trachtigt, da die kritischen Bereiche zum Flaschenhals wer-den konnen. Dies gilt insbesondere dann, wenn viele Thre-ads verwendet werden und die kritischen Bereiche eine gro-be Granularitat haben, also relativ lang sind.

Fur heutige Multicore-Prozessoren spielt dieses Pro-blem noch eine untergeordnete Rolle, da nur wenige Pro-zessorkerne verwendet werden. Fur zukunftige Multicore-Prozessoren mit Dutzenden von Prozessorkernen oderbeim Zusammenschalten mehrere Multicore-Prozessoren zuClustersystemen 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

Page 159: Multicore Parallele Programmierung Kng617

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 ausgefuhrt wird und bei deren Ausfuhrungfolgende Eigenschaften gelten:

• Serialisierbarkeit: Die Transaktionen eines Pro-gramms erscheinen fur alle beteiligten Threads sequen-tiell angeordnet; kein Thread beobachtet eine Ver-schrankung von Instruktionen verschiedener Transak-tionen; fur jeden Thread erscheinen die Transaktionenin der gleichen Reihenfolge.

• Atomaritat: Die von den Instruktionen einer Trans-aktion durchgefuhrten Anderungen des gemeinsa-men Speichers werden fur die die Transaktion nichtausfuhrenden Threads erst am Ende der Transaktionatomar 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 aufden gemeinsamen Speicher direkt sichtbar wird. Die Ver-wendung des Transaktionsmechanismus ist also nicht nureine Programmiertechnik, sondern kann auch andere Er-gebnisse als ein Sperrmechanismus bewirken.

Die Verwendung von Transaktionen erfordert dieEinfuhrung neuer Konstrukte, etwa auf Sprachebene. Dafurwurde die Einfuhrung von atomic-Blocken zur Identifikati-on von Transaktionen vorgeschlagen [1]: anstatt der Ver-wendung einer Sperrvariablen wird ein Sprachkonstruktatomic{B} vorgeschlagen, das die Anweisungen in BlockB als Transaktion ausfuhrt.

Die im Rahmen des HPCS-Projektes entwickelten Spra-chen - Fortress von Sun [4], X10 von IBM [14] und Cha-

Page 160: Multicore Parallele Programmierung Kng617

152 7 Weitere Ansatze

pel von Cray [18] - enthalten solche Konstrukte zur Un-terstutzung von Transaktionen.

Der Unterschied zwischen der Verwendung von Sperrva-riablen und atomaren Blocken ist in Abb. 7.1 am Beispieleines threadsicheren Kontozugriffs veranschaulicht. Einsperrorientierter Zugriff wird durch die Klasse LockAccountmit Hilfe eines Java synchronized-Blocks realisiert. Ein Auf-ruf von add() leitet den Aufruf einfach an die gleichnamigeMethode der nicht-threadsicheren Account-Klasse weiter,die wir hier als gegeben voraussetzen. Die Ausfuhrung dessynchronized-Blocks bewirkt die Aktivierung eines Sperr-mechanismus bzgl. des Objekts mutex; dieser stellt eineSequentialisierung des Zugriffs sicher. Ein transaktionsori-entierter Zugriff konnte durch die Klasse AtomicAccountrealisiert werden, die einen atomic-Befehl verwendet, umdie Aktivierung der nicht-threadsicheren add()-Methodeder Account-Klasse als Transaktion zu identifizieren. Da-mit ware das Laufzeitsystem fur die Sicherstellung der Se-rialisierbarkeit und Atomaritat verantwortlich, musste abernicht unbedingt eine Sequentialisierung erzwingen, wenndies nicht notwendig ist. Dabei ist zu beachten, dass atomic-Blocke (noch) nicht Teil der Java-Sprache sind.

Der Vorteil der Verwendung von Transaktionen liegtdarin, dass das Laufzeitsystem auch mehrere Transaktio-nen parallel zueinander ausfuhren kann, wenn das Spei-cherzugriffsmuster der Transaktionen dies zuließe. Bei Ver-wendung einfacher Sperrvariablen ist dies nicht ohne weite-res moglich. Sperrvariablen konnen zwar verwendet werden,um komplexere Synchronisationsmechanismen zu definie-ren, die den gleichzeitigen Zugriff mehrerer Threads erlau-ben, dies erfordert aber einen erheblichen zusatzlichen Pro-grammieraufwand. Ein Beispiel sind Lese-Schreibsperren,die mehrere Lesezugriffe gleichzeitig, aber jeweils nur einenSchreibzugriff erlauben, siehe Abschnitt 4.4. Fur die Ver-

Page 161: Multicore Parallele Programmierung Kng617

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.

Page 162: Multicore Parallele Programmierung Kng617

154 7 Weitere Ansatze

wendung von Transaktionen wird damit eine bessere Ska-lierbarkeit erwartet als bei der Verwendung von Sperrvaria-blen.

Die Verarbeitung von Transaktionen stellt bestimmteAnforderungen an das Laufzeitsystem:

• Versionskontrolle: Der Effekt einer Transaktion darferst am Ende der Transaktion sichtbar werden. Da-mit muss das Laufzeitsystem wahrend der Abarbeitungeiner Transaktion auf einem separaten Datensatz ar-beiten. Wird die Transaktion abgebrochen, bleibt deralte Datensatz erhalten. Bei erfolgreicher Ausfuhrungder Transaktion wird der neue Datensatz am Ende derTransaktion global sichtbar.

• Erkennen von Konflikten: Sollen mehrere Transak-tionen zur Verbesserung der Skalierbarkeitseigenschaf-ten konkurrierend ausgefuhrt werden, muss sicherge-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 aktivesForschungsgebiet und so wird sicherlich eine geraume Zeitvergehen, bis der Ansatz in Standard-Programmiersprachenverwendet werden kann. Der Ansatz wird jedoch als vielver-sprechend eingeschatzt, da er einen abstrakteren Mechanis-mus als Sperrvariablen zur Verfugung stellt, der Problemewie Deadlocks vermeidet, zu einer guten Skalierbarkeit vonthreadbasierten Anwendungsprogrammen fuhren kann unddurch den Programmierer leichter anwendbar ist.

Page 163: Multicore Parallele Programmierung Kng617

Literatur

1. A. Adl-Tabatabai, C. Kozyrakis, and B. Saha. Unlockingconcurrency. 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. IntelPress, 2006.

4. Eric Allen, David Chase, Joe Hallett, Victor Luchangco,Jan-Willem Maessen, Sukyoung Ryu, Guy L. Steele, Jr., andSam Tobin-Hochstadt. The Fortress Language Specification,version 1.0beta, March 2007.

5. R. Allen and K. Kennedy. Optimizing Compilers for ModernArchitectures. Morgan Kaufmann, 2002.

6. G. Amdahl. Validity of the Single Processor Approach toAchieving Large-Scale Computer Capabilities. In AFIPSConference 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 ParallelComputing Research: A View from Berkeley. Technical Re-port UCB/EECS-2006-183, EECS Department, Universityof California, Berkeley, December 2006.

Page 164: Multicore Parallele Programmierung Kng617

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. SpringerVerlag, 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. MorganKaufmann, 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, Proceedingsof 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. MorganKaufmann, 1999.

18. D. Callahan and B. L. Chamberlain and H. P. Zima. TheCascade 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.

Page 165: Multicore Parallele Programmierung Kng617

Literatur 157

20. J. Doweck. Intel Smart Memory Access: MinimizingLatency 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. IntelTechnology 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 – EineEinfuhrung. Oldenbourg Verlag, 2007.

27. J. Held and J. Bautista ans S. Koehl. From a Few Cores toMany – A Tera-Scale Computing Research Overview. IntelWhite 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. Runger. Task Pool Teams: A Hybrid Pro-gramming Environment for Irregular Algorithms on SMPClusters. Concurrency and Computation: Practice and Ex-perience, 18(12):1575–1594, 2006.

31. C.A.R. Hoare. Monitors: An Operating Systems StructuringConcept. 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.

Page 166: Multicore Parallele Programmierung Kng617

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, September2005.

35. St. Kleiman, D. Shah, and B. Smaalders. Programming withThreads. Prentice Hall, 1996.

36. G. Koch. Discovering Multi-Core:Extending the Benefits ofMoore’s Law. Intel White Paper, Technology@Intel Maga-zine, 2005.

37. P.M. Kogge. An Exploitation of the Technology Space forMulti-Core Memory/Logic Chips for Highly Scalable Paral-lel Systems. In Proceedings of the Innovative Architecture forFuture Generation High-Performance Processors and Sys-tems. IEEE, 2005.

38. M. Korch and T. Rauber. A comparison of task pools fordynamic 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 withPthreads. 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 TechnologyArchitecture 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.

Page 167: Multicore Parallele Programmierung Kng617

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 ThermalManagement 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 andDistributed 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 ReportPNNL-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, May2005.

54. D.A. Patterson and J.L. Hennessy. Computer Organizati-on & Design — The Hardware/Software Interface. MorganKaufmann, 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. IEEEMicro, pages 117–125, 2003.

58. R.M. Ramanathan. Intel Multi-core Processors: Leading theNext Digital Revaluation. Intel White Paper, Technology-Intel Magazine, 2005.

Page 168: Multicore Parallele Programmierung Kng617

160 Literatur

59. T. Rauber and G. Runger. Parallele Programmierung, 2teAuflage. 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 turntoward 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-EnabledMulti-Core: Advantages of a Thread-Oriented Approach.Technology & Intel Magazine, December 2005.

Page 169: Multicore Parallele Programmierung Kng617

Index

Granularitat, 23

Mutex-Variable, 49

Amdahlsches Gesetz, 37atomares Objekt, 93Atomaritat, 151atomic-Block, 148

Barrier-Synchronisation,47

Bedingungs-Synchronisation,49

Bedingungsausdruck, 70Bedingungsvariable, 50,

101, 118in java.util.concurrent,

118in Pthreads, 70

Benutzer-Thread, 42

Betriebssystem-Thread,42

Cell-Prozessor, 17schematischer Aufbau,

19Chapel, 149Chip-Multiprocessing, 25Client-Server-Modell, 57

Datenparallelitat, 56Deadlock, 52, 69

Effizienz, 37

False Sharing, 26Flynnsche Klassifikation,

27Fork-Join, 55

in OpenMP, 125Fortress, 147

Gesetz von Moore, 1

Page 170: Multicore Parallele Programmierung Kng617

162 Index

Global Arrays, 149

HPCS Programmierspra-chen, 147

Hyperthreading, 5

Intel Core 2, 13Intel Tera-scale Compu-

ting, 11

JavaInterface Executor, 122Thread-Pool, 122atomare Operation, 120Barrier, 116Interface Condition, 118Interface Lock, 117Semaphore, 115

Java-Threads, 61, 85–123Klasse Thread, 86Mutexvariable, 92Scheduling, 113Signalmechanismus,

101Synchronisation, 91

java.util.concurrent, 115

Kommunikation, 62Kontextwechsel, 40Koordination, 46Kosten eines parallelen

Programmes, 36kritischer Bereich, 48, 150

in OpenMP, 139

logischer Prozessor, 5

Mapping, 23

Master-Slave, 57Master-Worker, 57Matrix-Multiplikation

in OpenMP, 134Microsoft.NET, 61MIMD, 28MISD, 27Monitor, 51Moore Gesetz, 1MPI, 57, 62Multicore

Cell-Prozessor, 17Hierarchischen Design,

8Intel Core 2, 13Netzwerkbasierten

Design, 10Pipeline-Design, 9

Multicore-Prozessor, 6Multiprocessing, 25Multitasking, 24Multithreading

Hyperthreading, 5simultanes, 5

Mutexvariable, 150in Java, 92in Pthreads, 66

mutual exclusion, 48

Nebenlaufigkeit, 25nichtdeterministisches

Verhalten, 48

OpenMP, 61, 125–144atomare Operation, 140default Parameter, 128kritischer Bereich, 139

Page 171: Multicore Parallele Programmierung Kng617

Index 163

parallele Schleife, 131paralleler Bereich, 127,

135private Parameter, 128reduction Parameter,

141schedule Parameter,

132shared Parameter, 128Sperrmechanismus, 142

parallele Laufzeit, 35parallele Schleife, 31

in OpenMP, 131paralleler Bereich, 56

in OpenMP, 127paralleles Programmier-

modell, 29paralleles System, 29Parbegin-Parend, 55Parbegin-Parend-

Konstrukt, 55Pipelining, 58Prioritatsinversion, 114Produzenten-

Konsumenten,60

ProgrammiermodellMaster-Slave, 57

Prozess, 40Prozessorkern, 7Pthreads, 61

Bedingungsvariable, 70Deadlock, 69Erzeugung von Thre-

ads, 64Mutexvariable, 66

Sperrmechanismus, 68Puffermechanismus, 120

Rechenressourcen, 23Rechner mit gemein-

samem Speicher,28

Rechner mit verteiltenSpeicher, 28

Reduktionsoperationin OpenMP, 141

Scheduling, 23Java-Threads, 113Prioritatsinversion, 114

Semaphor, 50Sequentialisierung, 49, 52Serialisierbarkeit, 151Signalmechanismus

in Java, 101SIMD, 27, 56simultanes Multithrea-

ding, 5, 25SISD, 27Skalierbarkeit, 38SMT, 5Speedup, 36Sperrmechanismus, 49

in Java, 92in java.util.concurrent,

117in OpenMP, 142in Pthreads, 66

Sperrvariable, 49, 66, 92,117, 150

SPMD, 56Synchronisation, 46

Page 172: Multicore Parallele Programmierung Kng617

164 Index

mit Java-Threads, 91

Task, 23, 122Taskpool, 59

Pthread-Implementierung,79

Thread, 33in Java, 85

in OpenMP, 125in Pthreads, 63Zustand, 45

Threads, 411:1-Abbildung, 44

N:M-Abbildung, 45

N:1-Abbildung, 44Transaktionsspeicher, 150

Unified Parallel C, 146

voll-synchr. Objekt, 93von-Neumann-Rechner,

27

wechselseitiger Aus-schluss, 48

Win32 Threads, 61

X10, 148

zeitkritischer Ablauf, 47