Multicore Parallele Programmierung Kng617
Upcoming SlideShare
Loading in...5
×
 

Multicore Parallele Programmierung Kng617

on

  • 2,234 views

 

Statistics

Views

Total Views
2,234
Views on SlideShare
2,234
Embed Views
0

Actions

Likes
2
Downloads
24
Comments
0

0 Embeds 0

No embeds

Accessibility

Categories

Upload Details

Uploaded via as Adobe PDF

Usage Rights

© All Rights Reserved

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Processing…
Post Comment
Edit your comment

Multicore Parallele Programmierung Kng617 Multicore Parallele Programmierung Kng617 Document Transcript

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