MultiCore-Programmierung in Java

Transcription

MultiCore-Programmierung in Java
MultiCore-Programmierung in Java
Bachelor-Arbeit (SS 2010)
Betreuer: Dipl.-Ing. Dr. Hans Moritsch
Verfasser: Gerold Egger
Matr.-Nr. 8917007
Universität Innsbruck
[email protected]
2
Inhaltsverzeichnis
1 Einleitung........................................................................................................................................1
1.1 Möglichkeiten der Performance-Steigerung............................................................................1
1.1.1 Erhöhung der Taktfrequenz..............................................................................................1
1.1.2 Pipelines...........................................................................................................................2
1.1.3 RISC (Reduced Instruction Set Computer)......................................................................3
1.1.4 MMX-Technologie...........................................................................................................3
1.1.5 Cache-Speicher................................................................................................................3
1.1.6 Koprozessoren..................................................................................................................4
1.1.7 Harvard-Architektur.........................................................................................................4
1.2 Leistungs-Steigerung ..............................................................................................................4
1.2.1 Definition.........................................................................................................................4
1.2.2 Speedup und Effizienz.....................................................................................................5
1.2.3 Grenzen der Leistungs-Steigerung ..................................................................................6
2 Parallele Programmierung ..............................................................................................................8
2.1 Zielsetzungen paralleler Programmierung ..............................................................................8
2.2 Entwicklung eines parallelen Programmes .............................................................................9
2.3 Skalierbarkeit.........................................................................................................................10
2.4 Multicore-Prozessoren ..........................................................................................................11
2.5 Parallelrechner.......................................................................................................................12
2.5.1 Klassifikation nach der Art der Befehlsausführung.......................................................12
2.5.2 Klassifikation nach der Speicherorganisation................................................................14
2.6 Kommunikationsnetzwerke...................................................................................................14
2.7 Kopplungsgrad von Mehrprozessor-Systemen......................................................................14
2.7.1 Eng gekoppelte Mehrprozessorsysteme.........................................................................14
2.7.2 Lose gekoppelte Systeme ..............................................................................................15
2.7.3 Massiv parallele Rechner ..............................................................................................15
2.8 Parallelisierungsstrategien.....................................................................................................15
2.8.1 Gebietszerlegung (domain decomposition) ...................................................................16
2.8.2 Funktionale Aufteilung (functional decomposition) .....................................................16
2.8.3 Verteilung von Einzelaufträgen (task farming) .............................................................16
2.8.4 Daten-Pipelining............................................................................................................17
3
2.8.5 Spekulative Zerlegung...................................................................................................18
2.8.6 Hybrides Zerlegungsmodell...........................................................................................18
2.9 Kommunikationsmodelle.......................................................................................................18
2.9.1 Shared-Memory-Programmierung.................................................................................18
2.9.2 Message-Passing-Programmierung................................................................................18
3 Unterstützung des parallelen Programmierens in Java .................................................................20
3.1 Klassische Thread-Programmierung......................................................................................20
3.2 Thread-Programmierung mittels Thread-Pools.....................................................................23
3.3 Fork/Join-Framework............................................................................................................25
4 Performance-Messungen...............................................................................................................31
4.1 Durchführung der Experimente.............................................................................................31
4.2 Problemstellung: Numerische Integration.............................................................................31
4.3 Problemstellung: Sortieren....................................................................................................37
4.4 Problemstellung: Fibonacci-Zahlen.......................................................................................42
4.5 Problemstellung: Multiplikation von Matrizen......................................................................47
4.6 Problemstellung: Jacobi-Relaxation......................................................................................50
5 Zusammenfassung.........................................................................................................................53
Anhang A (Programme zur Problemstellung „Numerische Integration“).......................................54
Anhang B (Programme zur Problemstellung „Sortieren“)..............................................................67
Anhang C (Programme zur Problemstellung „Fibonacci-Zahlen“).................................................78
Anhang D (Programme zur Problemstellung „Matrizen-Multiplikation“)......................................87
Anhang E (Programme zur Problemstellung „Jacobi-Relaxation“)..............................................105
6 Literaturverzeichnis.....................................................................................................................108
4
Kurzfassung
Sowohl Multicore-Prozessoren als auch die Programmiersprache Java erfreuen sich großer Beliebtheit. Von Multicore-Prozessoren erwartet man sich revolutionäre Performance-Steigerungen. Während man mit herkömmlichen Lösungen zur Leistungs-Steigerung schon an den physikalischen
Grenzen angelangt ist, steht die Verwendung von Multicore-Prozessoren – zumindest im PC-Bereich – erst am Beginn. Die Erwartungen in diesen neuen Lösungsansatz sind groß. Allerdings darf
bei aller Euphorie nicht darauf vergessen werden, daß auch auch entsprechende softwareseitige Anpassungen erforderlich sind. Inwiefern die beliebte Programmiersprache Java dazu geeignet ist adäquate Anwender-Software zu entwickeln, soll diese Arbeit untersuchen.
1 Einleitung
In den letzten Jahren haben sich auch im PC-Bereich Multi-Core-Prozessoren durchgesetzt. Neben
der Voraussetzung, daß das Betriebssystem im Stande ist, mehrere CPUs für das ProzessScheduling zu benutzen, müssen auch die Anwendungs-Programme so programmiert sein, daß
parallele Abarbeitungen von Programmteilen möglich sind. Dazu kommen oft speziell entwickelte,
optimierte, parallele Algorithmen zu Einsatz.
Multi-Core-Prozessoren erlauben es, das Paradigma des parallelen Programmierens umzusetzen. In
der parallelen Programmierung werden Programme in Abschnitte zerlegt, die parallel (nebenläufig)
ausgeführt werden können. Diese Programmabschnitte müssen im Allgemeinen synchronisiert
werden. Primäres Ziel des parallelen Programmierens ist die Leistungssteigerung.
Wenn in dieser Arbeit von Leistung (engl.: performance) gesprochen wird, ist die Schnelligkeit
gemeint, mit der die Programme abgearbeitet werden, und nicht etwa der Funktionsumfang oder die
Zuverlässigkeit eines Systems.
1.1 Möglichkeiten der Performance-Steigerung
Zur Programmierung von Hochleistungsanwendungen für Multicore-Prozessoren können höhere
Programmiersprachen eingesetzt werden. Im Gegensatz dazu konzentrieren sich frühere Bestrebungen die Leistung zu erhöhen auf hardwarenahe Komponenten bzw. auf Eigenschaften der RechnerArchitektur.
1.1.1 Erhöhung der Taktfrequenz
Seit jeher wurde versucht, eine bessere Performance durch höhere Taktfrequenzen zu erreichen.
Dabei ist man mittlerweile an das Limit eines vernünftigen Betriebes der Prozessoren gestoßen, da
der Aufwand, die Prozessoren zu kühlen, kaum mehr zu handhaben ist. Das zeigt sich an den
überdimensionalen Kühlkörpern, die auf den Prozessoren angebracht sind (siehe Abbildung 1).
Allerdings wird bezüglich der Wärmeentwicklung versucht gegenzusteuern, indem man die CoreSpannung (= die Spannung die der eigentliche Prozessor benutzt) wesentlich niedriger setzt, als die
I/O-Spannung, die vom Motherboard bereitgestellt wird. Die geringere Spannung bewirkt eine geringere Wärmeentwicklung.
Einleitung
2
Abbildung 1: Prozessorkühler für Sockel 775 (Intel Pentium D) mit
Heatpipe im Vergleich zu einem Kühler für den Sockel 7 (Intel Pentium
1 MMX) – (Quelle: Wikipedia)
1.1.2 Pipelines
Die Abarbeitung von Maschinen-Befehlen erfolgt in mehreren Phasen (Pipeline-Stufen), die nicht
zwingenderweise unmittelbar nacheinander erfolgen müssen. In jeder Phase wird eine Teilaufgabe
eines Befehls erledigt. So können die ersten Phasen eines weiteren Befehls bereits abgearbeitet werden, während der vohergehende Befehl noch seine letzten Phasen durchläuft. Es kommt so zu einer
Überlappung der Teilaufgaben unterschiedlicher Befehle (siehe Abbildung 2). Dadurch steigt der
Befehlsdurchsatz. Zu Konflikten (hazards) kommt es, wenn unter den Befehlen Abhängigkeiten
existieren. Dadurch wird die Parallelität eingeschränkt.
Superskalare Prozessoren
Werden in einen Prozessor mehrere Pipelines eingebaut, die parallel arbeiten können, so erhält man
superskalare Prozessoren. Die Anzahl aller, im besten Fall parallel laufender, Pipelines gibt den
Grad der Superskalarität an.
Einleitung
3
Abbildung 2: Pipelining – (Quelle: Wikipedia)
1.1.3 RISC (Reduced Instruction Set Computer)
Die RISC-Technologie verwendet Prozessoren mit einem reduzierten Befehlssatz. Dadurch kann
die CPU schneller arbeiten, weil sie nur auf wenige Befehle spezialisiert ist.
Das konträre Verfahren stellt das CICS-Verfahren (Complex Instruction Set Computer) dar. Hierbei
bietet der Prozessor auch komplexe Instruktionen an, die mittels eines Microcode-Progamms erst in
die eigentlichen Verarbeitungsschritte umgesetzt werden.
1.1.4 MMX-Technologie
Der Prozessor verfügt über Befehle, die speziell für Multimedia-Aufgaben geeignet sind. Programme, die auf diesen Befehlssatz angepasst sind, können effizienter arbeiten, da durch diese Multimedia-Befehle Aufgaben in weniger Einzelschritte unterteilt werden müssen.
1.1.5 Cache-Speicher
Mit Cache-Speichern wird versucht, bereits vorhandene Daten für eventuelle weitere Zugriffe vorrätig zu halten. Cache-Speicher liefern die Daten wesentlich schneller, als der Hauptspeicher. Allerdings ist Cache-Speicher sehr teuer und daher nur in beschränktem Ausmaß vorhanden. Aufgrund
Einleitung
4
dessen ergibt sich ein weiteres Problem, nämlich das der geeigneten Cache-Strategie. Die Cache-Strategie soll so gewählt werden, daß die Treffer-Rate (gefundene Daten im Cache-Speicher im
Verhältnis zu allen Anfragen an den Cache-Speicher) möglichst hoch ist.
1.1.6 Koprozessoren
Koprozessoren entlasten die CPU bei Fließkomma-Berechnungen. Falls die CPU über keine Fließkomma-Einheit verfügt, müssen solche Berechnungen von der CPU in viele Ganzzahl-Berechnungen zerlegt werden, was wesentlich länger dauert. Die CPU übernimmt die Steuerfunktionen. Der
Koprozessor kann nur zusammen mit einem normalen Prozessor arbeiten.
1.1.7 Harvard-Architektur
Im Gegensatz zur klassischen Von-Neumann-Architektur werden bei der Harvard-Architektur Daten
und Befehle in physikalisch getrennten Speichern gehalten. Beide Speicher werden von eigenen
Bus-Systemen angesteuert. Somit können Befehle und Daten gleichzeitig geladen und geschrieben
werden.
1.2 Leistungs-Steigerung
Als Maß für die Leistung soll in dieser Arbeit die Ausführungszeit von Programmen verstanden
werden. Dabei soll natürlich nur die reine Rechenzeit berücksichtigt werden und nicht etwa der
Zeitaufwand für Ein- und Ausgabe dazu gezählt werden. Ein algorithmusspezifischer Aufwand wie beispielsweise das Erstellen von Daten-Strukturen, auf denen der Algorithmus arbeitet - soll
allerdings in die Ausführungszeit mit aufgenommen werden.
1.2.1 Definition
Die Performance p x eines Programms x verhält sich umgekehrt proportional zur Laufzeit t x
des Programms:
p x ~1/t x
Ein Programm y ist um den Faktor c schneller als das Programm x wenn c=t x /t y gilt.
Einleitung
5
Die Performance hängt hauptsächlich von folgenden Faktoren ab:
•
dem verwendeten Algorithmus,
•
der verwendeten Programmier-Sprache,
•
dem Compiler und seinen Optimierungs-Einstellungen,
•
dem Instruktionssatz des Prozessors,
•
der Taktfrequenz, und
•
der benötigten Takt-Zyklen pro Anweisung.
1.2.2 Speedup und Effizienz
Speedup ist die Beschleunigung eines Programms gegenüber der seriellen (sequentiellen) Ausführung. Speedup ist somit ein geeignetes Maß, um die Wirksamkeit der parallelen Abarbeitung zu
messen. Für die Messung des Speedups wird das gleiche Problem auf unterschiedlich vielen Prozessoren gelöst, und die Ausführungszeiten (Laufzeiten der Programme) verglichen. Die Größe des Gesamt-Problems bleibt konstant, aber die Problemgröße eines einzelnen Prozessors sinkt mit wachsender Anzahl an Prozessoren. Der Speedup eines parallel ausgeführten Programms auf p Prozessoren errechnet sich mit
S  p = T 1 / T  p
T(1) … sequentielle Aufführungszeit
T(p) … Ausführungszeit bei Verwendung von p Prozessoren
Ein idealer Speedup würde eine lineare Kennlinie über der Anzahl der Prozessoren zeigen.
Der reale Speedup liegt etwas darunter, weil
sich mit steigender Anzahl verwendeter Prozessoren der Overhead vergrößert, (siehe Abbildung 3).
Unter Effizienz versteht man den Speedup in
Relation zur Anzahl der eingesetzten Prozessoren p:
E  p = S  P  / p
Abbildung 3: Idealer und realer Speedup
(Quelle: hikwww2.fzk.de)
Einleitung
6
Das Höchstmaß an Effizienz ist 1. In der Realität sinkt die Effizienz mit steigender Anzahl an verwendeten Prozessoren.
1.2.3 Grenzen der Leistungs-Steigerung
In diesem Abschnitt soll die Frage betrachtet werden, inwieweit es sinnvoll ist, die Anzahl der
parallel arbeitenden Prozessoren beliebig zu erhöhen. Durch mehrere parallel laufende Prozesse
bzw. Threads ergibt sich ein Verwaltungs-Overhead. Das betrifft die Aufteilung des
Gesamtproblems in Teilprobleme und die grundsätzliche Untersuchung nach Parallelisierbarkeit,
die durch Abhängigkeiten begrenzt ist. Ebenso stellt die Synchronisierung sowie die
Kommunikation der Prozesse bzw. Threads einen limitierenden Faktor dar – besonders bei
unterschiedlich langen Laufzeiten der zu synchronisierenden Prozesse bzw. Threads.
Gesetz von Amdahl
Programme können in der Praxis nie vollständig parallel abgearbeitet werden, weil sie oft sowohl
Abschnitte beinhalten, die zeitlich voneinander abhängig sind, als auch solche, die sequentiell
abgearbeitet werden müssen. Das heißt, es gibt einen Teil α eines Programms, der sich
parallelisieren läßt, und einen restlichen Teil 1−α  der sich nicht parallelisieren läßt. Wäre ein
Programm, wie im Idealfall, vollständig parallelisierbar, dann wäre die Beschleunigung bei p
Prozessoren 1/ p
gegenüber der rein sequentiellen Abarbeitung. Da dieser Idealfall praktisch
nicht zu erreichen ist, nehmen wir an, der parallelisierbare Anteil lasse sich um einen Faktor c
beschleunigen.
Der
Speedup
S(p)
errechnet
sich
dann
folgendermaßen:
S  p = 1 / 1−α  α /c 
Das Gesetz von Amdahl zeigt, daß der nicht parallelisierbare Anteil eines Programms eine
Leistungsbegrenzung darstellt, sodaß trotz beliebig vieler eingesetzter Prozessoren keine
Geschwindigkeits-Steigerung mehr erreichbar ist. Ist der nicht parallelisierbare Anteil eines
Programms 20 % der seriellen Gesamt-Laufzeit, dann konvergiert der maximal erreichbare Speedup
gegen eine obere Schranke von 5. Durch den Einsatz weiterer Prozessoren würde sich die
Programm-Laufzeit nicht mehr verringern, und die Effizienz ginge gegen Null.
Das Gesetz von Amdahl stellt eine pessimistische Abschätzung des maximalen Speedups dar, da es
nicht berücksichtigt, daß durch den Einsatz weiterer Prozessoren auch weitere Ressourcen – wie
Einleitung
7
größere Cache-Speicher – zur Verfügung stehen. Dies könnte sogar zu einem super-linearen
Speedup führen (allerdings stellt sich die Frage, ob Amdahl derartige Neben-Effekte in seinem
Modell überhaupt berücksichtigen wollte).
Gesetz von Gustafson
Während Amdahl die Größe des zu lösenden Problems als konstant betrachtet, sieht Gustafson die
Laufzeit des Programms als konstant an, und erklärt, daß durch die steigende Anzahl eingesetzter
Prozessoren größere Probleme gelöst werden könnten. Bei zunehmender Problemgröße wird der serielle Anteil eines Programms gegenüber dem parallelen immer kleiner und unbedeutender. Unter
dieser Betrachtungsweise kann man keine Grenze mehr festlegen, ab der eine größere Anzahl an
Prozessoren nicht mehr sinnvoll wäre.
Gustafson definiert den Speedup als
S  p = 1−α   p∗α wobei α wieder den parallelen An-
teil des Programms darstellt, und p die Anzahl der Prozessoren ist. Das Gesetz von Gustafson ist allerdings nicht bei Algorithmen geeignet, die einen hohen seriellen Anteil haben, oder für Programme, die in festgelegten Zeitgrenzen antworten müssen (Echtzeit-Anwendungen).
Parallele Programmierung
8
2 Parallele Programmierung
Aus dem Programmier-Paradigma der parallelen Programmierung ergeben sich zwei grundsätzliche
Problemstellungen [5]:
•
die Zerlegung des Programms in parallelisierbare Abschnitte
•
die Synchronisation parallel ablaufender Programmteile
Von parallelisierbaren Transaktionen (Prozesse oder Threads) spricht man, wenn die parallele
(eventuell verzahnte) Ausführung zum selben Ergebnis führt, wie die sequentielle Ausführung.
Nebenläufigkeit bedeutet, daß Transaktionen nebeneinander ausgeführt werden können, aber nicht
zwingend tatsächlich parallel ablaufen müssen. Es kann auch eine scheinbare gleichzeitige
Abarbeitung vorliegen wie bei Time-Sharing-Systemen. Als Multi-Tasking bezeichnet man die
Nebenläufigkeit von mehreren Prozessen. Multi-Threading ist die Nebenläufigkeit von mehreren
Threads (innerhalb eines Prozesses).
Die Parallelisierung kann …
•
explizit durch den Programmierer erfolgen, oder
•
implizit, automatisch durch den Compiler.
Die Parallelisierung durch den Programmierer erfordert eine gut überlegte Auswahl des geeigneten
(parallelen) Algorithmus bzw. eine genaue Analyse des Problems. Diese Problem-Analyse kann
auch maschinell durch einen Compiler erfolgen. Compiler, die derartige Analysen durchführen sind
schwer zu bauen. Außerdem kann angenommen werden, daß der Programmierer die Parallelität
seines Programms besser überblickt, als eine Software. Automatische Parallelisierung kommt vor
allem auf Ebene der Kontrollstrukturen zum Einsatz. Für größere Programmkomponenten – wie
Funktionen/Unterprogramme – sollte der Programmierer eingreifen. Für die kleinste Einheit – den
einzelnen Befehlen – kann der Prozessor für Parallelität in Form von Pipelining sorgen [31].
2.1 Zielsetzungen paralleler Programmierung
Die parallele Programmierung verfolgt folgende Ziele [18]:
1. Ausgleichen der Rechenlast
2. Die notwendige Kommunikation soll im Verhältnis zum Rechenaufwand gering sein.
Parallele Programmierung
9
3. Sequentielle Engpässe verringern
4. Gewährleistung der Skalierbarkeit
Ausgleichen der Rechenlast
Durch die Möglichkeit, Teilprobleme gleichzeitig zu bearbeiten, verringert sich die Last an jedem
Prozessor-Kern.
Kommunikation im Verhältnis zum Rechen-Aufwand
Es erscheint nicht sinnvoll, die Zerlegung des Gesamtproblems in Teilprobleme beliebig feiner
Granularität fortzusetzen. Ab einem gewissen Stadium (das vom Verflechtungsgrad der TeilProbleme abhängt), übersteigt der Kommunikations-Aufwand die Zeitersparnis der parallelen
Verarbeitung. Wie hoch der passende Grad an Granularität ist, hängt hauptsächlich vom
Algorithmus ab aber auch von der Plattform.
Sequentielle Engpässe
Sequentielle Engpässe ergeben sich aus zeitlichen Abhängigkeiten zwischen einzelnen Tasks.
2.2 Entwicklung eines parallelen Programmes
Der Entwurfs-Prozess zur Entwicklung paralleler Programme kann in vier Phasen aufgeteilt werden
(siehe Abbildung 4):
1. Partitionierung
2. Kommunikation
3. Agglomeration (Anhäufung)
4. Mapping
Partitionierung und Kommunikation machen aus dem
Gesamt-Problem nebenläufige, skalierbare Algorithmen. Agglomeration und Mapping sorgen für eine
möglichst gleichmäßige und performante Aufteilung
der Last auf die CPUs. Durch Feinabstimmung können Algorithmen optimiert werden, um die Leistung
zu steigern [33].
Abbildung 4:
4 Phasen des Entwurfs-Prozesses
(Quelle: http://kbs.cs.tuberlin.de/ivs/Lehre/SS04/VS/)
Parallele Programmierung
10
Partitionierung
Die Aufteilung des Gesamt-Problems in kleinere Teil-Probleme nennt man Partitionierung. Diese
kann grundsätzlich entsprechend den Daten erfolgen (domain decomposition, data decomposition)
oder nach den Funktionalitäten (functional decomposition) [40].
Kommunikation
In der Kommunikations-Phase wird untersucht, ob und wie die einzelnen Teilprobleme untereinander Informationen austauschen müssen. Dadurch entsteht wiederum eine Verflechtung der zuvor
isolierten Tasks. Die Tasks müssen untereinander koordiniert werden.
Agglomeration
Hierbei werden kleine Tasks wiederum zu größeren zusammengefaßt, wenn dadurch der Kommunikations-Aufwand vermindert werden kann, oder die Performance verbessert werden kann. Diese
Phase dient der Vorbereitung für ein sinnvolles Mapping, d.h. eine möglichst gleichmäßige Aufteilung der Problem-Größen auf die Prozessor-Kerne.
Mapping
Die Zuteilung der Teilprobleme (Tasks) auf die zur Verfügung stehenden Prozessoren/Prozessor-Kerne wird als Mapping bezeichnet. Die Zuteilung soll so erfolgen, daß die Ressourcen möglichst gut (d.h. gleichmäßig) ausgelastet sind. Durch Kommunikation stark verflochtene Tasks sollen am gleichen Prozessor-Kern ausgeführt werden, um den Overhead an Kommunikation klein zu
halten.
2.3 Skalierbarkeit
Ein wichtiges Kriterium für den Einsatz eines Mehrprozessor-Systems ist die Skalierbarkeit eines
Problems. Skalierbarkeit untersucht den Ressourcen-Bedarf eines Programms. Ein Programm ist
gut skalierbar, wenn es mit n-mal so vielen Prozessoren ein n-fach größeres Problem in gleicher
Zeit lösen kann, bzw. wenn das Programm mit n-mal so vielen Prozessoren das gleiche Problem in
einem n-tel der ursprünglichen Zeit lösen kann. Neben dieser Skalierbarkeit der Rechenzeit kann
man analog eine Skalierbarkeit des Speicherbedarfs definieren. Aufgrund des KommunikationsOverhead zwischen den Prozessen bzw. Threads werden in der Praxis diese idealen Werte kaum
erreicht [29].
Parallele Programmierung
11
Um die Skalierbarkeit eines Anwendungsprogramms zu erhöhen können Maßnahmen ergriffen
werden, wie das Cachen von Inhalten, langsame Zugriffe auf Datenträger auf später verschieben,
synchrone Aufrufe vermeiden, die das System blockieren usw.
Bei
Multiprozessor-Systemen
spricht
man
von
vertikaler
Skalierung.
Hier
wird
die
Leistungsfähigkeit eines einzelnen Rechners erhöht. Im Gegensatz dazu wird bei der horizontalen
Skalierung die Last auf zusätzliche Rechner verteilt. Das Blade-Konzept unterstützt einen solchen
Ansatz. Blade-Server sind Baugruppen von Prozessoren mit eigener Hauptplatine samt
Arbeitsspeicher. Die Platinen werden in Slots eingeführt, und nutzen gemeinsam die Netzteile des
Baugruppenträgers [30].
2.4 Multicore-Prozessoren
Parallele Algorithmen benötigen zu ihrer parallelen Abarbeitung auch die entsprechende Hardware
in Form von mehreren CPUs. Diese können als gekoppelte Rechner vorliegen, oder als MulticoreProzessoren. Die Multicore-Variante ist kostengünstiger, als der Einbau mehrerer Prozessor-Chips.
Als Multicore-Prozessoren werden Prozessoren bezeichnet, die über mehrere CPUs (Central
Processing Units) - auf einem einzigen Chip – verfügen. Es handelt sich um mehrere vollständige
Prozessoren inklusive eigener arithmetisch-logischer Einheit (ALU), Registersätze und - sofern
vorhanden - Floating Point Unit (FPU). Nach der Anzahl der vorhandenen Cores spricht man von
Dual-Core-, Triple-Core-, Quad-Core-Prozessoren usw. Unix, der SMP-Linux-Kernel und
Microsoft Windows ab XP unterstützen Multicore-Prozessoren [31].
Nach dem Aufbau bzw. der Funktionsweise der Prozessoren lassen sich symmetrische und
asymmetrische Multicore-Prozessor-Systeme unterscheiden [18]:
•
Symmetrische Multicore-Prozessoren:
In symmetrischen Multicore-Prozessor-Systemen sind alle Kerne gleichartig. Das bedeutet,
daß Programme auf beliebigen Kernen ausgeführt werden können.
•
Asymmetrische Multicore-Prozessoren:
In asymmetrischen Multicore-Prozessor-Systemen existieren verschiedenartige Kerne. Die
Kerne haben unterschiedliche Maschinensprachen, und übernehmen unterschiedliche Aufgaben. Einige Kerne arbeiten wie Hauptprozessoren, andere wie Koprozessoren. Ein Pro-
Parallele Programmierung
12
gramm kann nur auf einem solchen Kerntyp ausgeführt werden, für den es geschrieben
wurde.
2.5 Parallelrechner
Parallelrechner kommen vor allem für Simulationen zum Einsatz. Rechnerisch aufwändige Simulationen werden hauptsächlich in den Natur- und Ingenieurs-Wissenschaften benötigt. Mit derartigen
Simulationen können reale Experimente ersetzt werden. Das ist oft wesentlich kostengünstiger als
das reale Experiment (z.B. Crashtests bei Autos) [21].
Parallelrechner zeichnen sich dadurch aus, daß mehrere Rechner mit der Lösung der gleichen Aufgabe beschäftigt sind. Man kann verschiedene Architiektur-Modelle von Parallelrechnern unterscheiden. Analog dazu ergeben sich – je nach Rechnerarchitektur - entsprechende Programmiermodelle [28].
Parallelrechner lassen sich folgendermaßen klassifizieren [32]:
•
nach der Art der Befehlsausführung (Klassifikation nach Flynn)
•
nach der Speicherorganisation (verteilter Speicher oder gemeinsamer Speicher)
2.5.1 Klassifikation nach der Art der Befehlsausführung
Hierbei werden die Rechner-Architekturen nach der Kombination von ein oder mehreren Befehlsströmen und ein oder mehreren Datenströmen unterteilt.
Kriterien
gleiche Instruktion
single instruction
unterschiedliche Operationen
multiple instruction
gleicher Datensatz
single data (SD)
SISD
MISD
unterschiedliche Datensätze
multiple data (MD)
SIMD
MIMD
Klassifikation nach Flynn
(Quelle: http://www.rz.uni-karlsruhe.de/rz/hw/sp)
Parallele Programmierung
13
Beschreibung der vier Architekturen:
•
SISD (Single Instruction, Single Data):
Ein Befehl verarbeitet einen Datensatz (herkömmliche Rechnerarchitektur eines seriellen
Rechners, wie PCs oder Workstations in Von-Neumann- oder Harvard-Architktur).
•
SIMD (Single Instruction, Multiple Data):
Ein Befehl verarbeitet mehrere Datensätze, n Prozessoren führen zu einem Zeitpunkt den
gleichen Befehl, aber mit unterschiedlichen Daten aus (Vektorrechner und Prozessor-Arrays
für Spezial-Anwendungen). Auf allen Prozessoren läuft das gleiche Programm mit unterschiedlichen Daten. Diese Architektur zeichnet sich durch eine große Portabilität und einfache Verwaltung aus, da es nur ein ausführbares Programm gibt (siehe Abbildung 5).
Abbildung 5: SIMD (Quelle: http://kbs.cs.tu-berlin.de/ivs/Lehre/SS04/VS/)
• MISD (Multiple Instruction, Single Data):
Mehrere Befehle verarbeiten den gleichen Datensatz, und führen somit redundante Berechnungen durch. Diese Rechnerarchitektur ist nie realisiert worden.
• MIMD (Multiple Instruction, Multiple Data):
Unterschiedliche Befehle verarbeiten unterschiedliche Datensätze. Auf jedem Prozessor
kann ein anderes Programm laufen. Diese Architektur ist flexibel, aber schwieriger zu programmieren, als die zuvor genannten. MIMD ist das Konzept fast aller modernen Parallelrechner.
Parallele Programmierung
14
2.5.2 Klassifikation nach der Speicherorganisation
•
Parallel-Rechner mit gemeinsamem Hauptspeicher:
Alle Prozessoren greifen auf einen gemeinsamen Hauptspeicher zu. Bei vielen Prozessoren
ist die Zeit für den Hauptspeicherzugriff der limitierende Faktor.
•
Parallel-Rechner mit verteiltem Speicher:
Jeder Prozessor kann nur auf seinen eigenen lokalen Speicher zugreifen. Der Zugriff auf Daten anderer Prozessoren erfolgt über ein Kommunikationsnetzwerk. Alternativ kann das Betriebssystem den gesamten Hauptspeicher als Virtual Shared Memory betrachten.
2.6 Kommunikationsnetzwerke
Die Leistungsfähigkeit eines Parallelrechners wird wesentlich durch das Kommunikationsnetz bestimmt. Man unterscheidet unidirektional und bidirektional nutzbare Netzwerke. Weiters wird die
Leistung durch die Topologie des Netzwerkes bestimmt. So kann es bei bestimmten Topologien
auch zu Kommunikations-Engpässen kommen (z.B. bei Baum-Strukturen an der Wurzel) [32].
2.7 Kopplungsgrad von Mehrprozessor-Systemen
Bei Systemen mit enger Kopplung nutzen die Prozessoren einen gemeinsamen Arbeitsspeicher. In
Systemen mit loser Kopplung verfügen die Prozessoren jeweils über einen eigenen Arbeitsspeicher.
Von massiv parallelen Rechnern spricht man, wenn eine große Zahl von Prozessoren (bis zu
mehreren tausend) mit jeweils etwas Arbeitsspeicher über ein dichtes Netz mit individuellen, sehr
schnellen Verbindungen gekoppelt sind [32].
2.7.1 Eng gekoppelte Mehrprozessorsysteme
Bei eng gekoppelten Mehrprozessorsystemen greifen wenige (derzeit 2 bis 16) Prozessoren auf
einen geteilten großen Arbeitsspeicher zu. Sie befinden sich an einem Ort und benutzen einen gemeinsamen Speicherbus.
Parallele Programmierung
15
2.7.2 Lose gekoppelte Systeme
Um einen guten Grad an Parallelisierung zu erreichen, sind Systeme anzustreben, die einen
geringen Grad an Abhängigkeiten ihrer Komponenten aufweisen (lose gekoppelte Systeme). So
kann jede CPU ihre Arbeit für sich erledigen, ohne daß zu viel Aufwand an Kommunikation
betrieben werden muß.
Bei lose gekoppelten Mehrprozessorsystemen (loosely-coupled multi-processor system) verfügt
jeder Prozessor über einen eigenen (lokalen) Arbeits-Speicher. Die Prozessoren kommunizieren
über geteilte Verbindungen in der Form lokaler Netze oder Clusternetze.
2.7.3 Massiv parallele Rechner
Bei massiv parallelen Rechnern (massively parallel systems) sind eine große Zahl von Prozessoren
(bis zu mehreren tausend) mit etwas Arbeitsspeicher in einem dichten Netzwerk mit individuellen,
sehr schnellen Verbindungen gekoppelt. Die Anzahl und die Übertragungskapazität der Verbindungen steigen mit der Zahl der verbundenen Prozessoren [32].
2.8 Parallelisierungsstrategien
Bevor man sich über Parallelisierung eines Codes Gedanken macht, sollte man nach der optimalen
sequentiellen Variante suchen. Erst wenn der sequentielle Code optimiert ist, sollte man mit der
Parallelisierung beginnen.
Parallelisierungsbestrebungen
werden
durch
Abhängigkeiten
behindert.
Dabei
können
Abhängigkeiten zwischen den Daten bestehen oder auch innerhalb des angewandten Algorithmus.
Parallelisierungsstrategien basieren auf dem Prinzip des Zerlegens („divide and conquer“). Man
unterscheidet eine Datenzerlegung und eine funktionale Zerlegung. Schwierigkeiten bereiten vor
allem Problemstellungen, die Kommunikation zwischen den Prozessen bzw. Threads voraussetzen.
Hier ist es schwierig, den passenden Grad an Parallelität zu finden. Zu starke Parallelisierung
könnte dazu führen, daß der Aufwand für Kommunikation höher wird, als die Einsparung, die durch
Parallelisierung erreicht wird, sodaß das Programm im Endeffekt langsamer laufen würde als bei
sequentieller Abarbeitung. Deshalb ist es auch üblich, einen Schwellwert (Threshold) für die
Problemgröße festzulegen, ab dem eine Problemstellung erst parallel verarbeitet wird. Ist die
Problemgröße noch unterhalb des Schwellwertes, wird eine sequentielle Verarbeitung bevorzugt
[18].
Parallele Programmierung
16
2.8.1 Gebietszerlegung (domain decomposition)
Diese Zerlegung basiert auf der Datenparallelität. Das Problem läßt sich in Teilprobleme aufteilen,
wie beispielsweise die Abszissen-Abschnitte bei der numerischen Integration. Derartige Problemstellungen sind die angenehmsten, weil sich ihre Zerlegung durch die Natur der Sache ergibt. Die
Teilprobleme werden dann möglichst gleichmäßig auf alle verfügbaren Prozessorkerne aufgeteilt.
Dabei ist – neben der Anzahl der Teilprobleme - auch die Problemgröße der Teilprobleme zu berücksichtigen. Auf jeden Kern sollte nicht primär die gleiche Anzahl an Teil-Problemen fallen, sondern es sollte vor allem jeder Kern die möglichst gleiche Gesamt-Problemgröße zu bewältigen haben. Zu diesem Zweck kann eine zyklische Aufteilung der Teilprobleme gegenüber einer blockweisen Aufteilung zielführender sein.
2.8.2 Funktionale Aufteilung (functional decomposition)
Diese Zerlegung basiert auf der Kontrollparallelität. Bei der funktionalen Aufteilung wird das
Gesamtproblem in kleinere, weniger komplexe Teilprobleme zerlegt. Die Zerlegung erfolgt in der
Weise, daß jedes Teilproblem eine geschlossene Funktionalität darstellt. Das heißt, die Teilprobleme
sind weitgehend voneinander unabhängig. Die Teilprobleme können also als Funktionen betrachtet
werden. Die Funktionen des Programms (z.B. bei Strömungsberechnungen) werden auf
verschiedene Prozessoren aufgeteilt.
2.8.3 Verteilung von Einzelaufträgen (task farming)
Hier übernimmt ein Master-Prozess die Verwaltungsarbeiten. Der Master zerlegt das Gesamtproblem in mehrere kleinere Tasks, und verteilt diese an die Slave-Prozesse. Ebenso ist der Master für
das Einsammeln der Ergebnisse der Slaves zuständig und für die Berechnung des Gesamt-Ergebnisses. Zwischen den einzelnen Slaves findet normalerweise keine Kommunikation statt.
Die Lastverteilung kann statisch oder dynamisch erfolgen. Bei statischer Lastverteilung werden die
einzelnen Tasks am Beginn der Berechnung auf die Slaves verteilt, sodaß der Master vorübergehend
frei von Verwaltungsarbeiten ist und eventuell auch einen Task übernehmen kann. Bei der dynamischen Lastverteilung hingegen werden die Tasks flexibel an die Slaves zugeteilt, je nach der Auslastung der Slaves. Dieses Vorgehen ist vorteilhaft, wenn die Anzahl der Tasks nicht im vorhinein bekannt ist, oder die Anzahl der Tasks die Anzahl der zur Verfügung stehenden Slaves übersteigt, oder
wenn die Ausführungszeiten der Tasks nicht voraussagbar sind bzw. sehr unausgewogen sind, und
somit vorerst keine balancierte Lastverteilung innerhalb der Slaves besteht [33].
Parallele Programmierung
17
Task-Farming kann einen hohen Skalierbarkeitsgrad erreichen, jedoch kann der Master-Prozess
einen Engpass bedeuten. Daher kann es sinnvoll sein, mehrere Master einzusetzen, wobei dann jeder von ihnen für eine Gruppe von Slaves zuständig ist.
Abbildung 6: Task-Farming (Master-Slave)
Quelle: http://kbs.cs.tu-berlin.de/ivs/Lehre/SS04/VS/
2.8.4 Daten-Pipelining
Daten-Pipelining basiert auf dem Prinzip der funktionalen Zerlegung. Dabei werden Berechnungen
in einzelne Phasen zerlegt (siehe Kapitel 1.1.2 Pipelines). Die Phasen können zeitlich überlappend
abgearbeitet werden. Die Effizienz ist abhängig von einer gleichmäßigen Auslastung der Pipelines.
Dieses Verfahren wird häufig in Datenreduktions- und Bildverarbeitungs-Anwendungen benutzt
[33].
Parallele Programmierung
18
Abbildung 7: Data-Pipelining
(Quelle: http://kbs.cs.tu-berlin.de/ivs/Lehre/SS04/VS/)
2.8.5 Spekulative Zerlegung
Bei Verzweigungen des Programm-Flusses können vorsorglich Programmzweige ausgewertet werden, obwohl noch nicht feststeht, daß sie tatsächlich gebraucht werden. Werden die Berechnungen
später nicht gebraucht, dann werden sie einfach verworfen. Dieses Verfahren kommt nur zum Einsatz, wenn keines der zuvor genannten Verfahren angewandt werden kann [33].
2.8.6 Hybrides Zerlegungsmodell
Das hybride Zerlegungsmodell wird auch als geschichtete Zerlegung bezeichnet. Es kommt bei
großen, umfangreichen Anwendungen zum Einsatz. Dabei wird das Gesamtproblem in Schichten
unterteilt, und für jede Schicht ein geeignetes Zerlegungsverfahren gewählt [33].
2.9 Kommunikationsmodelle
Prozesse einer parallelen Anwendung müssen im allgmeinen Daten austauschen und miteinander
kommunizieren. Diese Kommunikation erfolgt über ein schnelles Kommunikations-Netzwerk.
2.9.1 Shared-Memory-Programmierung
Die Parallelisierung erfolgt automatisch durch Compiler-Optionen, oder mittels Parallelisierungsdirektiven durch Verwendung von parallelen mathematischen Bibliotheken. Die Prozessor-Kommunikation geht direkt über einen schnellen breitbandigen Datenbus [33].
2.9.2 Message-Passing-Programmierung
Das Rechengebiet (domain) wird auf alle Prozessoren aufgeteilt (domain decomposition). Jeder
Prozessor rechnet lokal und kommuniziert über optimierte MPI-Funktionen mit den anderen Pro-
Parallele Programmierung
19
zessoren. Die Effizienz sinkt oft mit der Anzahl der Prozessoren, da die Interprozessor-Kommunikation stark zunimmt.
Kommunikationsmethoden [33]:
• Synchron, d.h. alle Tasks nehmen gleichzeitig an der Kommunikation teil.
• Asynchron, d.h. die Tasks führen die Kommunikationsoperationen zu unterschiedlichen
Zeitpunkten aus.
• Blockierend, d.h. der Task wartet, bis die Kommunikationsoperation beendet ist.
• Nicht blockierend, d.h. der Task stößt die Kommunikationsoperation an und führt dann simultan zur Kommunikation andere Operationen aus.
Unterstützung des parallelen Programmierens in Java
20
3 Unterstützung des parallelen Programmierens in Java
Betriebssysteme erlauben die Zuweisung von bestimmten CPUs für die Abarbeitung eines
Anwendungs-Programms (CPU-Affinity). Inwiefern es klug ist, den Prozess-Scheduler auf diese
Weise zu bevormunden, ist fraglich. Allerdings darf sich dieser Trend nicht in die AnwendungsEntwicklung von Java-Programmen fortsetzen, da sonst die angestrebte Plattform-Unabhängigkeit
von Java nicht mehr gegeben wäre. Es kann ja im vorhinein nicht gesagt werden, wie viele Cores
auf der Zielplattform zur Verfügung stehen werden. Das Programm muß auf einem SingleCoreProzessor ebenso lauffähig sein, wie auf beispielsweise einem Prozessor mit 64 Cores.
Java zählt zu den wenigen Programmiersprachen, bei denen von Anfang an für Parallelismus
vorgesorgt wurde. Und Java ist keineswegs eine Programmiersprache, in der Stillstand in der
technologischen Entwicklung herrscht. So sind in den rund 15 Jahren, in denen Java mittlerweile
schon zur Verfügung steht, viele Neuerungen dazugekommen – insbesondere auch Neuerungen, die
das parallele Programmieren betreffen. In den folgenden Abschnitten sollen diese erläutert werden.
3.1 Klassische Thread-Programmierung
Schon von Anfang an war Java für die parallele Programmierung durch das Konzept der Threads
gerüstet.
Threads
Threads sind Ausführungsstränge, sie werden auch als leichtgewichtige Prozesse bezeichnet.
Threads unterscheiden sich von (schwergewichtigen) Prozessen dadurch, daß sie sich untereinander
bestimmte Ressourcen teilen, was den Kontext-Switch beim Timesharing-Verfahren – das besonders bei Einprozessor-Systemen von Bedeutung ist – beschleunigt. Die gemeinsam verwendeten
Ressourcen sind das Code-Segment, das Daten-Segment, sowie die verwendeten Datei-Deskriptoren und Netzwerkverbindungen. Threads sind Teil eines (schwergewichtigen) Prozesses. Ein
Prozess kann nur einen einzigen Thread enthalten (z.B. der main-Thread in einem Java-Programm),
oder aus mehreren Threads (bei der Multithread-Programmierung). Threads „leben“ - wie Prozesse
– nur einmal. Sie haben die gleichen Zustände von „erzeugt“, „lauffähig“, „laufend“, „blockiert“
Unterstützung des parallelen Programmierens in Java
21
bzw. „unterbrochen“ bis „beendet“. Soll ein gleichartiger Task ein weiteres Mal laufen, muß er neu
als Thread instanziert werden, und mit der start()-Methode zum Laufen gebracht werden [34].
Wie Threads implementiert sind, hängt von der Java Virtual Machine (JVM) ab, und in weiterer
Folge dann, wie Threads vom darunter liegenden Betriebssystem unterstützt werden. Bei
Betriebssystemen, die Threads direkt unterstützen, kann die JVM Java-Threads direkt auf das
Betriebssystem
abbilden
(native
Threads).
Bei
anderen
Betriebssystemen
erfolgt
die
Implementierung der Java-Threads in Form sogenannter „green threads“. Green Threads werden
von der JVM emuliert, ohne dabei vom Betriebssystem unterstützt zu werden. Sie werden - im
Gegensatz zu nativen Threads - im User-Modus ausgeführt. Native Threads können auf preemptive
Art gewechselt werden, während green Threads blockieren müssen, oder explizit die Kontrolle
abgeben müssen, damit es zu einem Thread-Wechsel auf der CPU kommt. Der Trend geht weg von
Green-Threads hin zu nativen Threads [20].
Multi-Thread-Programmierung
Jedes lauffähige Java-Programm braucht eine main-Funktion, um gestartet werden zu können. Der
Inhalt dieser main-Funktion bildet den Inhalt des main-Threads, den vorläufig einzigen
Ausführungsstrang des Programms. Durch den Start neuer Threads kommen weitere
Ausführungstränge hinzu, die parallel zum main-Thread abgearbeitet werden. Bei MehrkernProzessoren kann ein Kern mit der Abarbeitung eines bestimmten Threads beauftragt werden. So
wird der Thread ohne Unterbrechung abgearbeitet. Stehen weniger Prozessor-Kerne als laufende
Threads zur Verfügung, muß der Schedulder des Betriebssystems die Abarbeitungs-Reihenfolge der
Threads regeln. Üblicherweise wird in dieser Situation das Time-Sharing-Verfahren (ZeitscheibenVerfahren) angewandt, bei dem in regelmäßigen Zeit-Intervallen (Zeit-Scheiben) ein Wechsel des
Threads stattfindet, der von der CPU bearbeitet wird. So ein Wechsel (Context-Switch) ist mit
einem Zeit-Aufwand verbunden, der nicht für die Abarbeitung von Threads genutzt werden kann.
Allerdings ist dieser Aufwand bei Threads wesentlich geringer, als bei (schwergewichtigen)
Prozessen.
Im Fall von mehreren Prozessor-Kernen, die zur Abarbeitung der Threads zur Verfügung stehen, ist
es einleuchtend, daß sich dadurch eine Geschwindigkeits-Steigerung ergibt. Allerdings kann MultiThread-Programmierung auch die Ausführung auf Single-Prozessor-System beschleunigen, wenn
die Threads unterschiedliche Ressourcen beanspruchen. Dabei kommt es auf eine passende
Unterstützung des parallelen Programmierens in Java
22
Kombination der quasi-parallel ausgeführten Threads an. So können bei lange dauernden
Operationen Wartezeiten sinnvoll für die Abarbeitung anderer Threads genutzt werden.
Thread und Runnable
Jede Klasse, die die Möglichkeit bieten soll, als Thread ausgeführt zu werden, muß die Schnittstelle Runnable implementieren. Java bietet eine Klasse Thread, die bereits diese Schnittstelle
implementiert. Die Schnittstelle Runnable schreibt (nur) die parameterlose Methode run() vor.
Klassen, die zur parallelen Abarbeitung vorgesehen sind, können auch von der Klasse Thread abgeleitet werden. Allerdings hat man sich dadurch die Vererbung aus anderen Klassen verbaut, da
Java keine Mehrfach-Vererbung unterstützt. Daher ist die Implementierung des Interfaces
Runnable der Weg, der eine weitere Ableitung offen läßt und daher zu bevorzugen ist. Wenn eine
parallele Abarbeitung in Gang gebracht werden soll, muß ein Thread-Objekt erzeugt werden.
Dazu erhält der Konstruktor des Thread das Objekt, das das Interface Runnable implementiert.
Damit der Thread auch in die Warteschlange des Prozess-Schedulers kommt, muß die start()Methode des Thread-Objekts aufgerufen werden. Die start()-Methode bewirkt den (internen)
Aufruf der run()-Methode laut Schnittstelle Runnable. Im Code schaut das so aus:
class BeispielKlasse implements Runnable { ... }
... BeispielKlasse beispielObjekt = new BeispielKlasse();
Thread thread = new Thread(beispielObjekt).start();
Wichtig dabei ist, daß der Benutzer nicht direkt die run()-Methode aufrufen darf. Ein solcher
Aufruf würde nicht zu einer parallelen Abarbeitung führen, sondern zu einer seriellen Ausführung.
Threads haben eine bestimmte Priorität. Ohne explizite Festlegung dieser Priorität ist sie gleich
hoch wie die Priorität des erzeugenden Threads. Java unterstützt 10 Prioritätsstufen. Für das
Scheduling entscheidend ist allerdings auch, wie viele Prioritätsstufen das Betriebssystem
unterstützt, und inwiefern der Scheduler des Betriebssystem Prioritäten überhaupt berücksichtigt.
Threads können auch als Daemon-Threads (Hintergrund-/Dienst-Threads) gekennzeichnet werden.
Daemon-Threads laufen auf unbestimmte Zeit weiter (d.h. über die Laufzeit des erzeugenden
Threads hinaus). Die Kennzeichnung eines Threads als Daemon hat mit der Methode
setDaemon() noch vor dem Aufruf der start()-Methode zu erfolgen.
Unterstützung des parallelen Programmierens in Java
23
Weiters können für Threads Namen vergeben werden. Bei der Vergabe kontrolliert weder der JavaCompiler noch die JVM ob die Namen eindeutig sind. Vergibt der Programmierer keinen Namen,
dann wird automatisch ein Name der Form Thread­<n> vergeben, wobei n eine laufende
Nummer ist. Außerdem können Threads zu Gruppen zusammengefaßt werden. Thread-Gruppen
erleichtern die Verwaltung bei einer großen Anzahl von Threads.
Wichtig für die Synchronisation von Threads ist die Methode join(). Damit können Threads
auf die Beendigung anderer Threads warten. Kritische Regionen können mit dem Schlüsselwort
synchronized vor mehrfacher Abarbeitung gesperrt werden. Die Sperre kann sich auf
Methoden, Blöcke oder Objekte beziehen. Eine feingranulare Sperre ist zu bevorzugen, um
Deadlocks möglichst zu vermeiden. Derartige Sperren schränken die Parallelität einer Anwendung
ein [20].
3.2 Thread-Programmierung mittels Thread-Pools
Die klassische Thread-Programmierung wie sie Java von Anfang an angeboten hat, ist gekennzeichnet durch einen niedrigen Abstraktionsgrad. Mit Java 1.5 wurden mit dem Package java.util.­
concurrent die „Concurrency Utilities“ eingeführt. Dieses Package beinhaltet high-level APIs
für nebenläufige Programmierung. Es gibt zwei Unterpakete zur Realisierung von kritischen Abschnitten: java.util.concurrent.atomic und java.util.concurrent.locks.
Die Concurrency Utilities bestehen aus folgenden Teilen [16]:
•
Concurrent Collections
Diese bieten fein-granularere Sperren, als das Collections Framework von Java 2.
•
Executor-Framework
Das Executor-Framework beinhaltet das neue Konzept der ThreadPools.
•
Synchronizers
•
Locks und Conditions
•
Atomic Variables
… zur atomaren Manipulation von Variablen
Unterstützung des parallelen Programmierens in Java
24
Executor-Klassen
Die Schnittstelle Executor schreibt eine execute()-Methode vor, der ein Objekt übergeben
wird, das die Schnittstelle Runnable implementiert. Implementierungen zum Executor-Interface sind ThreadPoolExecutor und ScheduledThreadPoolExecutor. In diesen Implementierungen werden Thread-Pools benutzt, um die häufige Erzeugung neuer Threads einzuschränken. Threads werden in den Pools nach getaner Arbeit nicht beendet, sondern halten sich bereit, um
weitere Aufgaben zu übernehmen. ScheduledThreadPoolExecutor ist für wiederholte Ausführung gleicher Threads gerüstet. Die Klasse Executors bietet Methoden zur Erzeugung und
Handhabung derartiger Objekte. Die Schnittstelle ExecutorService erweitert Executor, und
managt Queuing und Scheduling von Tasks, und erlaubt kontrolliertes Shutdown.
Threads mit Rückgabewert
Da Objekte, die Runnable implementieren, nur zur reinen Abarbeitung gedacht sind, wurde mit
Java 1.5 eine weitere Schnittstelle Callable eingeführt. Diese ermöglicht eine Rückgabe berechneter Ergebnisse. So wie Runnable die Methode run() vorschreibt, schreibt Callable call() als einzige parameterlose Methode vor, um das Ergebnis zu bestimmen. Über die get()Methode eines sogenannten Future-Objektes erhält man das Ergebnis. Die Methode get() blockiert solange, bis das Ergebnis zur Verfügung steht. Das Future-Objekt entsteht z.B. bei der
Übergabe des Callable-Objektes an ein ExecutorService-Objekt durch dessen Methode
submit(). Ein typisches Code-Segment sieht folgendermaßen aus:
/* Callable­Klasse schreiben */ class MyCallable implements Callable<ReturnType> { public ReturnType call() { /* Belegung des ReturnType­Objekts */ } } ... MyCallable task = new MyCallable( ReturnObj ); ExecutorService exe = Executors.newCachedThreadPool(); Future<ReturnType> result = exe.submit( task ); ReturnType resOb = result.get(); ...
ExecuterService bietet neben der Methode submit() noch die Methode invokeAll(),
mit der ganze Sammlungen (Collections) von Tasks zur Abarbeitung übergeben werden können.
Unterstützung des parallelen Programmierens in Java
25
Entsprechend wird dabei eine Liste von Future-Objekten erzeugt, um die einzelnen Rückgaben
später abfragen zu können.
3.3 Fork/Join-Framework
Für Java 1.7 ist bezüglich nebenläufiger Programmierung ein Fork/Join-Framework geplant. Das
Grundprinzip dieses Frameworks ist, Probleme, u.a. rekursiv, in Teil-Probleme aufzuteilen, die parallel gelöst werden, und deren Ergebnisse nach Beendigung der Tasks wieder zusammengestellt
werden (siehe Abbildung 9). Es soll ein leichtgewichtiges Java-Framework werden, das sich gut
portieren läßt, sodaß sich gute plattform-unabhängige Speedups ergeben. Neu an dieser Technologie
ist vor allem das Prinzip des Work-Stealings, die aufgeteilten Tasks dynamisch an die beteiligten
CPUs zu vergeben. Damit soll vermieden werden, daß auf die Beendigung der Arbeit einiger CPUs
noch gewartet werden muß, während andere Prozessoren bereits ohne Arbeit sind. Voraussetzung
für dieses Work-Stealing ist eine genügend fein-granulare Aufsplittung des Gesamtproblems [11].
Abbildung 8: Prinzip der Arbeitsweise des Fork/Join-Frameworks
(Quelle: http://5l3vgw.bay.livefilestore.com/)
Design des Fork/Join-Frameworks
Zuerst wird ein Pool von Worker-Threads eingerichtet. Damit können ausständige Tasks (die noch
abgearbeitet werden müssen) an Threads vergeben werden, ohne daß der Thread erst (aufwändig)
Unterstützung des parallelen Programmierens in Java
26
kreiert werden muß. Üblicherweise werden so viele Worker-Threads erzeugt, wie CPUs vorhanden
sind. Optimierungs-Überlegungen gehen dahin, daß man eventuell weniger Worker-Threads erzeugt, und somit CPUs für weitere (unvorhergesehene) Aufgaben frei läßt. Oder man erzeugt mehr
Worker-Threads, als CPUs zur Verfügung stehen, damit Threads, die ihre Aufgaben vorzeitig erledigt haben, gleich weitere Sub-Tasks zur Bearbeitung vorfinden. Die Zuteilung (mapping) der
Threads an die CPUs übernimmt die Java-Virtual-Machine (JVM) und das Betriebssystem.
Fork/Join-Tasks sind keine Instanzen klassischer Threads, sondern leichtgewichtiger AusführungsKlassen namens FJTask. Leichtgewichtig deshalb, weil für Worker-Threads einige Features überflüssig sind – die klassische Tasks haben – wie beispielsweise die Unterbrechungsbehandlung durch
I/O-Operationen. FJTask implementiert – gleich wie Thread – das Interface Runnable.
Ebenso wie bei klassischen Threads kann auch ein FJTask durch Übergabe eines Runnable-Objekts erzeugt (und gestartet) werden.
Die zur Verarbeitung anstehenden Tasks werden in Warteschlangen (Queues) verwaltet. Jeder Worker-Thread hat eine eigene Warteschlange. Das Fork/Join-Framework verwendet spezielle Warteschlangen namens Deque. Deque steht für double-ended-Queue. Das bedeutet, daß – anders als bei
normalen Queues - Tasks an beiden Enden der Queue entnommen werden können [37].
Die Klasse FJTaskRunnerGroup richtet die Pools von Worker-Threads ein, und startet die Ausführung der zugewiesenen Tasks. Diese Zuweisung erfolgt mit den Methoden invoke() bzw.
coInvoke(). Beide Methoden sorgen nicht nur für eine parallele Ausführung, sondern auch für
eine Synchronisation der übergebenen Threads. Im Code sieht das beispielsweise so aus:
class Fib extends FJTask { .....
FJTaskRunnerGroup group = new FJTaskRunnerGroup(2); Fib f = new Fib(35); group.invoke(f); .....
@Override public void run() { ...
// SubTasks erzeugen: Fib f1 = new Fib(n ­ 1); Fib f2 = new Fib(n ­ 2); // Tasks parallel ausführen und synchronisieren: coInvoke(f1, f2); ... Als Alternative zur Ableitung von der Klasse FJTaks, kann die Ableitung von der Klasse Fork­
JoinTask erfolgen. ForkJoinTask ist eine abstrakte Basisklasse für Tasks, die mithilfe der
Klasse ForkJoinPool ausgeführt werden. Subklassen von ForkJoinTask sind Recursi­
Unterstützung des parallelen Programmierens in Java
27
veAction und RecursiveTask, von denen der Benutzer seine Klassen ableiten kann. Recur­
siveAction stellt die ergebnislose Variante dar, während mit RecursiveTask ein Ergebnis
kreiert wird. Der Code ändert sich entsprechend:
class Fib extends RecursiveTask<Integer> { .....
ForkJoinPool pool = new ForkJoinPool(numTasks); Fib f = new Fib(num); pool.invoke(f); .....
@Override protected Integer compute() { // SubTasks erzeugen: Fib f1 = new Fib(n – 1); Fib f2 = new Fib(n – 2); // Tasks parallel ausführen und synchronisieren: invokeAll(f1, f2); Die Klasse ForkJoinTask stellt die leichtgewichtige Form eines Future-Objekts dar. Die
invoke()-Methoden sind semantisch einem fork- und join-Aufruf äquivalent. Das heißt die
Synchronisation der Tasks erfolgt automatisch.
ForkJoinTasks sollten relativ kleine Teil-Probleme lösen. Die Anzahl der Basis-Operationen pro
Task
sollte
idealerweise
zwischen
100
und
10000
betragen
(lt.
Doug
Lea
auf
http://gee.cs.oswego.edu/). Wenn die Tasks zu groß sind, kann der Parallelismus den Durchsatz
nicht mehr verbessern; sind sie zu klein, wirkt sich der Verwaltungs-Overhead der Tasks negativ auf
die Performance aus.
Work-Stealing
Eine wesentliche Neuerung des Fork/Join-Frameworks ist Work-Stealing. Das bedeutet, daß unbeschäftigte Threads (die ihre übertragenen Tasks schon beendet haben) sofort weitere Tasks aus den
Warteschlangen anderer Threads übernehmen.
Damit Work-Stealing optimal funktioniert, wird im Fork/Join-Framework die schon erwähnte spezielle Warteschlange Deque verwendet. Jeder fork-Aufruf stellt den neuen (kleineren) Task – anders als bei normalen Queues – an den Head der Deque (LIFO-Prinzip). Das hat den Effekt, daß die
aufwändigen Tasks am Tail der Deque landen, während sich die kleineren Tasks am Head befinden.
Worker-Threads holen sich die Tasks von ihrer eigenen Deque immer vom Head (nach dem LIFOPrinzip), während sich „stehlende“ Worker-Threads die Tasks immer vom Tail einer fremden Deque
Unterstützung des parallelen Programmierens in Java
28
– das ist die Warteschlange eines anderen Worker-Threads – holen. Das läuft dann nach dem FIFOPrinzip. Der Sinn dieser Vorgangsweise liegt darin, daß sich „stehlende“ Threads die relativ aufwändigen Tasks holen, und dadurch auch relativ lange beschäftigt sind. Dadurch reduzieren sich
aber auch die Zugriffe auf fremde Warteschlangen. Und da die Wahrscheinlichkeit gering ist, daß
zwei Threads gleichzeitig leere Warteschlangen haben, kommt es kaum zu konkurrenzierenden Zugriffen auf gleiche Tasks in fremden Warteschlangen [37].
Einordnen eines neuen Tasks in die Deque
beim einem fork-Aufruf (push-Operation):
Worker-Thread
Abarbeitung eines Tasks aus der eigenen Deque (pop-Operation):
Worker-Thread
Work-Stealing aus fremder Deque (take-Operation):
Worker-Thread A
Worker-Thread B
leere Deque
Als Folge des Work-Stealings laufen fein-granulare Anwendungen schneller, als grob-granulare.
Implementierung
Das Fork/Join-Framework wurde mit rund 800 Zeilen reinen Java-Codes implementiert. Dieser befindet sich hauptsächlich in der Klasse FJTaskRunner, einer Unterklasse von java.lang.
Thread.FJTasks. Die Klasse FJTaskRunnerGroup dient zum Konstruieren der WorkerThreads, verwaltet einige Zustands-Variablen (z.B. die Identitäten aller Worker-Threads, die für
Work-Stealing benötigt werden), und unterstützt koordiniertes Startup und Shutdown. (Quelle:
Doug Lea, http://gee.cs.oswego.edu/dl/papers/fj.pdf)
Unterstützung des parallelen Programmierens in Java
29
Im Zusammenhang mit Deques gibt es drei Operationen:
(1) push-Operation:
Mit fork kreierte neue Tasks legt der kreierende Worker-Thread mit einer push-Operation
an den Head seiner Deque.
(2) pop-Operation:
Jeder Worker-Thread entnimmt mit einer pop-Operation Tasks aus dem Head seiner Deque.
(3) take-Operation:
Hat ein Worker-Thread seine eigenen Tasks abgearbeitet (leere Deque), dann schaut er in
den Deques (zufälliger) anderer Worker-Threads nach, ob noch Tasks zur Verarbeitung anstehen. Wenn er solche findet entnimmt er sie mit einer take-Operation vom Tail der Deque.
Deques verwalten eine top-Variable, die auf den Head der Deque verweist, und eine base-Variable, die auf den Tail der Deque zeigt. top wird von push und pop inkrementiert bzw. dekrementiert, während base nur von der take-Operation verändert wird. Am Head wird die Deque also
wie ein Stack behandelt. Bei der Implementierung von Deques konnten Locks aus folgenden Gründen weitgehend vermieden werden:
•
push- und pop-Operationen werden nur vom Worker-Thread durchgeführt, der die betreffende Deque besitzt. Daher können diese Operationen an einer Deque nie gleichzeitig auftreten.
•
take-Operationen finden gegenüber push- und pop-Operationen relativ selten statt. Wenn
auf eine Deque eine take-Operation angewendet wird, muß nur verhindert werden, daß ein
weiterer Thread eine take-Operation auf die gleiche Deque ausführt.
•
Ein Konflikt zwischen pop- und take-Operation kann nur auftreten, wenn nur mehr ein
einziger Task in der Deque ist. Dieses Problem wurde elegant umgangen, indem bei den Abfragen, ob die Deque nur mehr ein einziges Element enthält der Pre-Inkrement- bzw. PreDekrement-Operator verwendet wurde.
•
push-Operation:
if (−−top >= base) ... take-Operation:
if (++base < top) ... push- und take-Operationen können zu keinen Konflikten führen.
Unterstützung des parallelen Programmierens in Java
30
Für die Zuordnung der Tasks zu CPUs ist die JVM verantwortlich. Es gibt keine Möglichkeit zu erfahren, ob die JVM einen Task immer gleich einer neuen CPU zuweist, sobald eine frei ist. Diese
Zuweisungen stellen daher eine unbekannte Größe bei Performance-Messungen dar. Ebenso ist die
Arbeit des Garbage-Collectors (GC) grundsätzlich ein nicht vorhersehbares Ereignis, das Performance-Messungen beeinflussen könnte. Allerdings ist der GC ein Prozess mit niedriger Priorität,
sodaß sich seine Aktivität auf die Messungen nicht gravierend auswirken dürfte. Auch die Verwendung größerer Daten-Typen dämpft den Speedup ein.
Das Fork/Join-Framework ist dafür optimiert, daß die Threads eine Lokalität aufweisen. Das heißt
der Zugriff eines Worker-Threads auf die eigene Deque erfolgt schneller, als der Zugriff auf fremde
Deques beim Work-Stealing. Deshalb ist das Work-Stealing auch dadurch eingeschränkt, daß hierbei die aufwändigeren Taks geholt werden, wodurch die Worker-Threads länger beschäftigt werden.
Die Worker-Threads verbringen also die meiste Zeit damit die Tasks aus ihrer eigenen Deque zu bearbeiten.
Performance-Messungen
31
4 Performance-Messungen
In diesem Abschnitt sollen Performance-Vergleiche der verschiedenen - unter Java zur Verfügung
stehenden – Technologien gemacht werden. Weiters werden unterschiedliche Problemstellungen anhand von Beispiel-Programmen betrachtet.
4.1 Durchführung der Experimente
Die Java-Programme wurden an einem MultiCore-Rechner mit 32 Cores an der Universität Innsbruck getestet. Das verwendete 32-Core-System ist eine SunFire X4600 M2 mit 8 Quad-Core AMD
Opteron-8356-Prozessoren.
Zur Bestimmung der Laufzeiten wurde jedes Programm mit den gleichen Argumenten (mindestens)
dreimal gestartet und daraus der mittlere der drei Werte notiert. Bei einer starken Streuung der gemessenen Werte (d.h. Abweichungen um mehr als ca. 10 %) wurden Bereichs-Angaben notiert. Die
Bereichs-Angaben geben den minimalen und maximalen Wert aus 12 gemessenen Werten an. Durch
diese Vorgehensweise soll einerseits vermieden werden statistische Ausreißer zu notieren, die keinen repräsentativen Wert darstellen, und andererseits eine repräsentative Größe für Streuungen abgebildet werden.
4.2 Problemstellung: Numerische Integration
Die numerische Integration ist ein Verfahren zur näherungsweisen Berechnung von Integralen, bei
der die zu berechnende Fläche entlang der Abszisse in gleich breite Abschnitte (Streifen) aufgeteilt
wird. Die Flächen-Berechnungen dieser Streifen sind unabhängig voneinander, und eigenen sich daher von Natur aus zur parallelen Berechnung. Bei der Zuteilung der Teil-Probleme (Berechnungen
der Streifen-Flächen) sollte allerdings darauf geachtet werden, daß die Größen der Teil-Probleme
wahrscheinlich unterschiedlich sein werden, da auch die Flächen von stark unterschiedlicher Größe
sein können. Dies ist auch im betrachteten Beispiel der Fall!
Es sind verschiedene Methoden bekannt, um die entsprechenden Abschnitts-Flächen dem tatsächlichen Verlauf der Kurve des Gesamt-Problems anzunähern. Theoretisch können hier Funktionen be-
Performance-Messungen
32
liebig hohen Grades zur Approximation verwendet werden. In der Praxis haben sich allerdings einfachere Methoden durchgesetzt:
Rechtecks-Methode
Die Höhe des Teil-Abschnittes der Fläche wird durch den Funktionswert des Mittelwertes der begrenzenden Abszissen-Werte angenommen. Dadurch erfolgt die Näherung der Fläche durch ein
Rechteck. Diese (einfachste) Methode der numerischen Integration wird im betrachteten Beispiel
angewandt.
Trapez-Methode
Im Unterschied zur Rechtecks-Methode werden die Funktionswerte an beiden Abszissen-Begrenzungen des Streifens berechnet, und durch eine Gerade verbunden. Dadurch ergibt sich ein Trapez
als Fläche des Teil-Abschnittes.
Simpson-Methode
Wie bei der Trapez-Methode werden beide Funktionswerte der vertikalen Begrenzungen berechnet,
aber hier durch eine Parabel-Kurve miteinander verbunden.
Näherungsweise Berechnung der Zahl π
Im betrachteten Beispiel-Programm wird die Zahl π (pi) wird mit Hilfe einer numerischen Integration durch die Formel 4 /1 x²  auf dem Intervall [0,1] berechnet.
Die Anzahl (nx) der Teil-Intervalle wird über die Konsole mit Hilfe der Methode getStripes() eingelesen. Zu jedem Teil-Intervall wird der Mittelpunkt auf der x-Achse berechnet, damit der Fehler - gegenüber dem tatsächlichen Integral - möglichst gut durch das berechnete Rechteck ausgeglichen wird. In einer Schleife werden zuerst nur die Ordinaten-Werte aufsummiert, und erst nach der
Schleife mit der Breite (dx) eines Teil-Intervalls multipliziert.
Die Dauer der Berechnung wird mit System.currentTimeMillis() gemessen und am
Ende des Programmes ausgegeben.
Sequentielle Variante
Das Programm PiCalcSeq.java berechnet das Integral sequentiell. Die anderen Versionen
führen eine parallele Berechnung mit verschiedenen Technologien durch. Bei der sequentiellen Be-
Performance-Messungen
33
rechnung steigt der Zeit-Aufwand erwartungsgemäß mit der Anzahl der Teil-Abschnitte ziemlich linear an.
Intervalle
Dauer in ms
100
210
200
405
300
600
400
792
3500
500
988
3000
600
1182
2500
700
1379
800
1568
900
1760
1000
1957
500
1100
2162
0
1200
2347
1300
2547
1400
2737
1500
2928
Laufzeit in ms
Sequentielle numerische Integration
2000
1500
1000
300 500 700 900 1100 1300 1500
200 400 600 800 1000 1200 1400
Anzahl der Teil-Intervalle
Parallele Varianten
Folgende Varianten der parallelen Berechnung wurden implementiert und getestet:
Das Programm PiCalcThreads1.java benutzt klassische Java-Threads, wie sie von Anfang
an in Java zur Verfügung stehen.
Die Berechnung soll parallel über mehrere Rechen-Einheiten (CPUs) erfolgen. Die Anzahl der verfügbaren Rechen-Einheiten wird dem Programm als Parameter übergeben. Die Teilintervalle werden gleichmäßig auf die verfügbaren Threads verteilt. Die weitere Berechnung erfolgt im Prinzip
gleich, wie beim sequentiellen Algorithmus.
Damit das Programm übersichtlich bleibt, erfolgt die Berechnung in einer eigenen Methode
calc(). Diese wird über eine Instanz dieses Programms aufgerufen, um sich aus dem statischen
Kontext der main()-Methode zu befreien.
Performance-Messungen
34
In der Berechnungs-Methode calc() wird für jede Rechen-Einheit eine Instanz einer ServiceKlasse eingerichtet. Diese Instanzen werden jeweils über einen separaten Thread (Thread-Array
serverThread) zur Ausführung gebracht.
Die Service-Klasse ist als innere Klasse implementiert. Da ihre Instanzen als Threads ausgeführt
werden, muß die Klasse das Interface Runnable implementieren. Die Service-Klasse erhält
über die Methode addValue(double) die Stellen, zu denen die Ordinaten-Werte berechnet
werden sollen. Die Berechnung erfolgt bei Ausführung (als Thread) in einer Schleife. Die einem
Service übergebenen Stellen sind nicht benachbart, sondern - zur besseren Last-Verteilung gleichmäßig über das gesamte Intervall [0, 1] verteilt. Es ist nämlich durchaus zu erwarten, daß der
Rechen-Aufwand allgemein im gesamten Intervall ungleichmäßig verteilt ist (wie auch in diesem
Beispiel-Programm).
Die Methode calc() wartet auf die Beendigung aller Service-Threads, und holt sich dann
von diesen über die Methode getResult() die Teil-Ergebnisse. Die Multiplikation der Funktions- Werte mit der Breite der Teil-Intervalle erfolgt hier zentral, da es für jeden Service-Thread
eine zusätzliche (unnötige) Rechen-Operation bedeuten würde. Das End-Ergebnis der Zahl π wird
schließlich an die main-Funktion übergeben, die die Ausgabe des Ergebnisses durchführt.
Die Variante PiCalcThreads2.java ist eine weitere Version, die klassische Threads benutzt.
Im Gegensatz zum vorigen Programm wird hier in der Service-Klasse kein Array mit den zu bearbeitenden Stellen angelegt, sondern die Service-Instanz berechnet sich selber die Stellen.
Das Programm PiCalcThreadPool.java benutzt die Concurrency-Utilities von Java 5. In
der Methode calc() werden mit Executors.newFixedThreadPool entprechend viele
Threads eingerichtet (soviel wie CPUs zur Verfügung stehen). Diese Threads werden von einem
Objekt der Klasse ExecutorService verwaltet.
Dieses ExecutorService-Objekt erhält über die Methode submit() Objekte für Teilberechnungen übergeben, die die Schnittstelle Callable implementieren. Um den Aufruf der entsprechenden call()-Methode muß sich der Programmierer nicht mehr kümmern, er erfolgt automatisch. Die Ergebnisse werden über Future-Objekte eingesammelt.
Die Future-Objekte enthalten nach erfolgreicher Berechnung die Teil-Ergebnisse, und stellen
diese über die blockierende Methode get() zur Verfügung.
Performance-Messungen
35
Im Programm PiCalcFJ1.java wird das Problem mit Hilfe des Fork/Join-Frameworks gelöst, das mit Java 7 in die Concurrency-Uitils integriert werden soll. Das benutzte Klassen-Modul
wird von der Klasse FJTask abgeleitet. Das Thread-Pool ist eine Instanz der Klasse
FJTaskRunnerGroup. Die Teil-Probleme (Tasks) werden in einem Objekt vom Typ Array­
List verwaltet.
PiCalcFJ2.java ist eine Variante zu PiCalcFJ1.java, in der die verwendete Klasse nicht
von FJTask abgeleitet wird, sondern von der Klasse RecursiveAction, die wiederum von
ForkJoinTask abgeleitet ist. Statt der run()-Methode ist eine compute()-Methode zu implementieren, und das Thread-Pool ist nicht mehr vom Typ FJTaskRunnerGroup, sondern vom
Typ ForkJoinPool. Ansonsten ist die Verarbeitung dem vorigen Beispiel sehr ähnlich.
Da die Laufzeiten für eine einzige Berechnung der Zahl π oft nur im Bereich von wenigen Milli-Sekunden waren, wurden die Berechnungen eine Million Mal wiederholt. Dadurch ergibt sich ein
Wertebereich der gemessenen Laufzeiten, in dem die gemessenen Zeiten besser miteinander verglichen werden können.
In der Wertetabelle werden folgende Abkürzungen der Programmnamen benutzt:
th1 .... PiCalcThreads1.java th2 .... PiCalcThreads2.java
tp .... PiCalcThreadPool.java
fj1 .... PiCalcFJ1.java fj2 .... PiCalcFJ2.java
Für 1000 Teil-Intervalle wurden folgende Laufzeiten (in ms) gemessen:
Threads
th1 th2
tp
fj1
fj2
1
7471
7792
1814
10434
9777
2
3789
4022
935
5112
4917
4
1917
2074
493
4790
4118
8
994
1049
272
3597
2971
16
536
552
177
2348
1908
32
371
324
121 ­ 994
1434
1317
64
276 ­ 1079
295
163 ­ 1318
1237
1596
128
191 ­ 910
187
196 ­ 1837
1246
1215
256
204 ­ 1644
186
174 ­ 1087
1449
1289
Performance-Messungen
36
Numerische Integration
1000 Teil-Intervalle
12000
Laufzeiten in ms
10000
th1
th2
tp
fj1
fj2
8000
6000
4000
2000
0
1
2
4
8
16
32
64
128
256
Anzahl der Threads
Die serielle Berechnung mit 1000 Teil-Intervallen und einer Million Wiederholungen dauerte 1975
ms. Im Vergleich zur seriellen Berechnung ergeben sich für die ThreadPool-Variante folgende
Speedups:
Speedups
(ThreadPool-Varinate)
12
(tp­Variante)
10
1
1,09
8
2
2,11
4
4,01
8
7,26
2
16
11,16
0
Speedup
Speedups Threads
6
4
1
2
4
8
16
Anzahl der Threads
Erkenntnisse aus den Messdaten
Laut obigen Messwerten scheinen beide Varianten des Fork/Join-Frameworks für diese Problemstellung weniger gut geeignet zu sein. Nur die ThreadPool-Varinate zeigt in allen gemessenen Werten ein besseres Laufzeit-Verhalten, als die serielle Berechnung. Die Thread-Varianten bringen erst
ab einer höheren Thread-Anzahl (von mehr als 4 Threads) eine Beschleunigung, wodurch der Einsatz von Threads nicht mehr effizient ist. Nachteilig erscheinen auch die starken Streuungen mancher Varianten bei Anwendung vieler Threads.
Performance-Messungen
37
4.3 Problemstellung: Sortieren
Sortier-Algorithmen eignen sich gut für parallele Verarbeitung, weil sie meistens nach dem „divide
& conquer“-Prinzip in Teil-Probleme zerlegt werden können. Das ist auch bei dem hier betrachteten
QuickSort-Algorithmus der Fall. Durch das jeweilige Zerteilen der zu sortierenden Listen in zwei
Teil-Listen entsteht eine Baum-förmige Aufteilungs-Struktur in immer kleinere Teil-Probleme, bis
die Listen nur mehr aus einem einzigen Element bestehen.
Sequentielle Variante
Das Programm QuickSortSeq.java führt den QuickSort-Algorithmus durch rekursiven Aufruf dieser Verteilungs-Strategie auf, jedoch ohne parallele Verarbeitung. In der sort()-Methode
wird für beide Teil-Listen jeweils wiederum die sort()-Methode (rukursiv) aufgerufen.
Die Laufzeiten vergrößern sich - lt. Messungen - deutlich mit der Anzahl der zu sortierenden Elemente, liegen aber im akzeptablen Rahmen.
Sequentielle Sortierung
Laufzeit in ms
10³
1
10⁴
25
10⁵
66
10⁶
196
10⁷
2109
(Quicksort-Algorithmus)
2500
2000
Laufzeit in ms
Anzahl der
Elemente
1500
1000
500
0
10³
10⁴
10⁵
10⁶
10⁷
Anzahl der Elemente
Parallele Varianten
Bei den parallelen Technologien zeigen sich markante Unterschiede im Laufzeit-Verhalten. Die
Messungen wurden mit 10⁶ zu sortierenden Elementen durchgeführt. Die Elemente sind vom FließKomma-Typ double, also jeweils 8 Byte groß.
Die Sortierung mittels klassischer Threads in QuickSortThreads.java zeigt, daß sich die
Laufzeiten verbessern je mehr Threads benutzt werden. Die Laufzeiten verbessern sich, bis sie – in
diesem Beispiel – das 500- bis 1000-fache der Anzahl der verfügbaren Cores erreichen. Wenn mehr
Performance-Messungen
38
Threads benutzt werden, erhöhen sich die Laufzeiten wieder leicht. Die optimale Laufzeit liegt offensichtlich bei einer relativ feinen Granularität. Folgende Werte wurden auf der 32-Core-Maschine
gemessen:
Laufzeit
in ms
2⁴
65358
2⁵
18198
2⁶
5462
2⁷
2100
2⁸
1056
2⁹
625
2¹⁰
351
2¹¹
246
2¹²
178
2¹³
155
2¹⁴
87
2¹⁵
88
2¹⁶
113
2¹⁷
148
2¹⁸
167
2¹⁹
215
2²⁰
210
2²¹
213
2²²
209
Quicksort-Algorithmus (10⁶ Elemente)
(Berechnung mit klassischen Java-Threads)
1200
1000
800
Laufzeit in ms
Anzahl der
Threads
600
400
200
0
2⁸
2⁹
2¹⁰
2¹¹
2¹²
2¹³
2¹⁴
2¹⁵
2¹⁶
2¹⁷
2¹⁸
2¹⁹
2²⁰
2²¹
2²²
Anzahl der Threads
Im gemessenen Bereich von ca. 4096 bis 262144 Threads ergibt sich für die Thread-Varinate hier
ein Speedup > 1. Dieser ist im folgenden Diagramm dargestellt. Die Effizienz in diesem Bereich ist
aber aufgrund der hohen Thread-Anzahl äußerst gering!
Nur ein Speedup > 1 stellt eine Beschleunigung dar. Daher muß in diesem Beispiel festgestellt werden, daß nur mit einer eingegrenzten Anzahl an verwendeten Threads eine Beschleunigung zu erreichen ist. Mit steigender Thread-Anzahl sinkt - ab ca. 32000 Threads - wieder der Speedup, was dadurch erklärt werden kann, daß die Erzeugung, die Verwaltung und das Beenden der Threads einen
Performance-Messungen
39
zusätzlichen Aufwand verursacht, der den gewünschten Effekt der parallelen Verarbeitung zunichte
macht.
Speedup
Sortierung von 10⁶ Elementen mittels Threads
2,5
Speedup
2
1,5
1
0,5
0
2048
4096
8192
16384
32768
65536
262144
524288
Anzahl der Threads
Effizienz
Sortierung von 10⁶ Elementen mittels Threads
0,00045
0,00040
0,00035
Effizienz
0,00030
0,00025
0,00020
0,00015
0,00010
0,00005
0,00000
2¹¹
2¹²
2¹³
2¹⁴
2¹⁵
Anzahl der Threads
2¹⁶
2¹⁷
2¹⁸
2¹⁹
Performance-Messungen
40
Im Gegensatz zur Variante, die klassische Threads benutzt, ändern sich die Laufzeiten bei den anderen Technologieen (Concurrency Utilities lt. Java 5 und Fork/Join-Framework) in Abhängigkeit von
der Anzahl der verwendeten Threads nicht wesentlich.
| L a u f z e i t e n / [ m s ]
Threads |QuickSortThreadPool QuickSortFJ1 QuickSortFJ2
­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
1 | 210 222 234
2 | 200 225 225
4 | 207 217 230
8 | 208 228 219
16 | 207 222 224
32 | 210 240 222
64 | 211 232 254
128 | 210 251 244
256 | 213 270 254
1024 | 210 405 287
2048 | ? 764 658
4096 | ? 2958 1455
8192 | ? 7313 8486
16384 | ? 8285 50651
32768 | 201 20692 ­
65536 | ? 81096 ­
1048576 | 206 ? ­
16777216 | 205 ? ­
­­­­­­­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
Die Klasse ForkJoinPool, die im Programm QuickSortFJ2 verwendet wird, ist offenbar nur
für 32767 Threads ausgelegt. Einträge, die in obiger Tabelle daher nicht ermittelt werden konnten
sind mit '­' gekennzeichnet. Die mit '?' gekennzeichneten Einträge wurden nicht ermittelt, weil entweder keine aufschlussreichen Werte zu erwarten sind, oder die Laufzeiten sowieso unattraktiv lang
wären.
Interessant ist auch der Vergleich der Fork/Join-Variante, die Objekte vom Typ FJTask und
FJTaskRunnerGroup benutzt (Programm QuickSortFJ1) und der Variante, die Objekte vom
Performance-Messungen
41
Typ ForkJoinTask und ForkJoinPool benutzt (Programm QuickSortFJ2). Die zweite
Variante verhält sich zuerst leicht vorteilhafter gegenüber der erstgenannten. Ab einer höheren Anzahl an verwendeten Threads klaffen die Laufzeiten aber stark auseinander, und die erstgenannte
Variante zeigt ein wesentlich besseres Laufzeitverhalten.
Vergleich der Fork/Join-Varianten
60000
50000
Laufzeit in ms
40000
30000
QuickSortFJ1
QuickSortFJ2
20000
10000
0
32
64
128
256
1024
2048
4096
8192 16384
Anzahl der Threads
Auch die Messung mit 10⁷ zu sortierenden Elementen zeigt, daß besonders die Technologie der
Concurrency Utilities (Programm QuickSortThreadPool) ziemlich gleichbleibende Laufzeiten aufweist, die von der Anzahl der Threads nahezu unabhängig ist:
Threads
Laufzeit/[ms]
1
2174
2
2164
4
2343
8
2173
16
2160
32
2125
64
2311
128
2271
256
2283
1024
2158
32768
2122
1048576
2125
16777216
2177
Performance-Messungen
42
4.4 Problemstellung: Fibonacci-Zahlen
Die Fibonacci-Folge ist eine unendliche Folge von Zahlen (den Fibonacci-Zahlen), bei der sich die
jeweils folgende Zahl durch Addition der beiden vorherigen Zahlen ergibt: 0, 1, 1, 2, 3, 5, 8, 13, …
Die Bestimmung einer Zahl aus dieser Folge macht es daher notwendig alle vorhergehenden Folgen-Glieder zu berechnen. Dadurch ergeben sich recht schnell aufwändige rekursive Berechnungen.
Daher ist dieses Beispiel ebenfalls für parallele Abarbeitung interessant.
Sequentielle Variante
Im Programm FibSeq.java wird die Fibonacci-Zahl zu einem bestimmten Argument durch
einen sequentiellen Algorithmus berechnet. Zur Berechnung einer Fibonacci-Zahl werden jeweils
die beiden vorangehenden Folgen-Elemente benötigt, war durch einen rekursiven Aufruf der Berechnungs-Funktion seqFib() geschieht.
Für den sequentiellen Algorithmus wurden folgende Laufzeiten gemessen:
n
Laufzeit in ms
Sequentielle Berechnung der Fibonacci-Zahlen
30
20
32
32
34
64
36
147
38
364
40
927
42
2409
1000
44
6262
0
46
16358
7000
Laufzeit(fib(n)) in ms
6000
5000
4000
3000
2000
30
32
34
36
38
40
42
44
n
Günstigerweise zeigen die gemessenen Ergebnisse nur eine sehr geringe Streuung, wodurch die
Laufzeiten aufgrund von Erfahrungswerten gut abschätzbar werden.
Performance-Messungen
43
Parallele Varianten
Durch die Verwendung von klassischen Threads im Programm FibThreads.java erhöhen sich
die Laufzeiten wesentlich. Solange das Argument n eine bestimmte Größe hat, werden Threads erzeugt. Die Verwendung von klassischen Java-Threads bringt hier keine Vorteile gegenüber der sequentiellen Berechnung.
Laufzeit in ms
20
17
22
47
24
115
26
257
28
726
30
3088
Berechnung von Fibonacci-Zahlen
(mit klassischen Java-Threads)
20000
Laufzeit(fib(n)) in ms
n
15000
10000
5000
0
20
22
24
26
28
30
32
n
32
18816
Auch die Verwendung der Concurrency Utilities in FibThreadPool.java bringt keine Verbesserung der Laufzeiten, und das Programm stößt – besonders bei wenigen Threads – bald an seine
Grenzen. Zu große - und daher nicht mehr gemessene - Werte sind in der Tabelle mit '>>' gekennzeichnet.
­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
| Threads
n | 32 64 128 256 512 1024 ­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
18 | 18 ms 19 ms 18 ms 19 ms 19 ms 19 ms 20 | 39 ms 43 ms 46 ms 45 ms 45 ms 45 ms 21 | >> 58 ms 63 ms 63 ms 63 ms 62 ms 22 | >> 79 ms 94 ms 98 ms 94 ms 93 ms 23 | >> >> 118 ms 136 ms 138 ms 138 ms 24 | >> >> >> 191 ms 215 ms 216 ms 25 | >> >> >> 252 ms 317 ms 360 ms 26 | >> >> >> >> 429 ms 623 ms 27 | >> >> >> >> 598 ms 935 ms 28 | >> >> >> >> >> 1290 ms ­­­­+­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
Performance-Messungen
44
Berechnung der Fibonacci-Zahlen
Concurrency Utilities (1024 Threads)
1400
Laufzeiten(fib(n)) in ms
1200
1000
800
600
400
200
0
20
21
22
23
24
25
26
27
28
Argument n der Fibonacci-Funktion
Die Fork/Join-Variante lt. Programm FibFJ1.java - in der die Klassen FJTask und FJTask­
RunnerGroup benutzt werden - zeigt schon wesentlich bessere Laufzeiten, als die vorher genannten parallelen Technologien. Das Diagramm zeigt, daß die Problemstellung – bis zu 16 verwendetenThreads - gut skaliert. Diese Anzahl entspricht der Hälfte der verfügbaren Cores.
Laufzeiten in ms in Abhängigkeit von n u. der Thread­Anzahl:
1 2 4 8 16 32 64 128 n Thread Threads Threads Threads Threads Threads Threads Threads
36
262
189
184
142
137
200
220
259
38
553
422
338
294
289
367
400
495
40
1219
775
605
450
394
485
655
804
42
2940
1566
1043
681
611
654
914
900
44
7380
3829
2250
1221
787
803
996
1067
46
18502
9323
5048
2732
1469
1072
1725
1531
Performance-Messungen
45
Berechnung der Fibonacci-Zahlen
Abhängigkeiten von der Anzahl an Threads
20000
18000
16000
Laufzeiten in ms
14000
1 Thread
2 Threads
4 Threads
8 Threads
16 Threads
12000
10000
8000
6000
4000
2000
0
36
38
40
42
44
46
n
Die zweite Fork/Join-Variante lt. Programm FibFJ2.java – die die Klassen ForkJoinTask und ForkJoinPool benutzt – skaliert ähnlich gut, wie die vorige Variante. Bei den in der Tabelle
mit '?' ausgefüllten Einträgen war eine Angabe der Messwerte nicht mehr sinnvoll, weil die gemessenen Werte stark gestreut waren.
Laufzeiten in ms in Abhängigkeit von n u. der Thread­Anzahl:
1 2 4 8 16 32 64 128 n Thread Threads Threads Threads Threads Threads Threads Threads
36
264
181
172
142
149
220
305
324
38
556
335
212
186
184
217
339
450
40
1251
711
401
284
231
239
?
?
42
3132
1570
863
523
349
258
?
?
44
7799
3832
1993
1117
644
508
?
?
46
19469
9480
4845
2594
1457
1101
?
?
Performance-Messungen
46
Beide Fork/Join-Varianten skalieren für Messwerte unter einer Zehntel Sekunde nicht. Vermutlich
fällt dort der Verwaltungs-Overhead der Threads mehr ins Gewicht, als die Zeitersparnis durch parallele Berechnung. Allerdings muß gesagt werden, daß im Bereich von unter einer Zehntel Sekunde
eine Verbesserung der Laufzeit für den Benutzer nicht merkbar ist, und daher auch nichts bingt.
Gegenüber der sequentiellen Laufzeit von 927 ms für die Berechnung von fib(40) ergeben sich für
die Fork/Join-Varianten folgende Speedup- und Effizienz-Werte:
FibFJ1
1 2 4 8 16 32 Th. Threads Threads Threads Threads Threads
64 Th.
128 Th.
Laufzeit
1219
775
605
450
394
485
655
804
Speedup
0,76
1,2
1,53
2,06
2,35
1,91
1,42
1,15
Effizienz
0,76
0,6
0,38
0,26
0,15
0,06
0,22
0,01
64 Th.
128 Th.
FibFJ2
1 2 4 8 16 32 Th. Threads Threads Threads Threads Threads
Laufzeit
1251
711
401
284
231
239
?
?
Speedup
0,74
1,3
2,31
3,26
4,01
3,88
?
?
Effizienz
0,74
0,65
0,58
0,41
0,25
0,12
?
?
Obwohl sich für beider Varianten Speedups ergeben, zeigen die Effizienz-Werte unter Berücksichtigung der dafür eingesetzten Threads (CPUs) keine besonders guten Ergebnisse. Immerhin würde
die ideale Effizienz bei 1 liegen.
Performance-Messungen
47
4.5 Problemstellung: Multiplikation von Matrizen
Die Multiplikation zweier Matrizen stellt ein Problem mit hohem Rechen-Aufwand dar. Der Aufwand steigt kubisch mit der Größe der Matrizen, das heißt mit der Anzahl ihrer Elemente. Die Komplexität von Matrizen-Multiplikationen – mit dem in folgenden Programmen angewandten Standard-Algorithmus - wird mit O(n³) angegeben.
Für die Problemstellung der Matrizen-Multiplikation wurden zwei sequentielle Algorithmen implementiert und getestet. Zum einen der normale (naive) Standard-Algorithmus und zum anderen ein
verbesserter Algorithmus namens Jama. Zunächst sollen die Laufzeiten dieser beiden Versionen verglichen werden. Die Tests wurden jeweils mit zwei gleich großen quadratischen N×N-Matrizen
durchgeführt.
Laufzeiten der Multiplikation zweier NxN-Matrizen
N
MatrixMult_naive.java MatrixMult_jama.java
50
12 ms
9 ms
100
31 ms
38 ms
200
44 ms
44 ms
300
82 ms
53 ms
400
265 ms
115 ms
500
647 ms
208 ms
600
1486 ms
365 ms
700
3036 ms
550 ms
800
5310 ms
1551 ms
900
9780 ms
2693 ms
1000
16495 ms
3700 ms
Matrizen-Multiplikation von 2 NxN-Matrizen
Vergleich zweier sequentieller Algorithmen
Laufzeit in ms
20000
15000
MatrixMult_naive.java
MatrixMult_jama.java
10000
5000
0
100
200
300
400
500
N
600
700
800
900
1000
Performance-Messungen
48
Erkenntnisse aus den Messdaten
Das Diagramm zeigt, daß die Laufzeiten des naiven Algorithmus – besonders bei großen Matrizen –
oft um ein Vielfaches höher sind, als die Laufzeiten des Jama-Algorithmus. Besonders bemerkenswert ist auch, daß der Anstieg der Kurve beim Jama-Algorithmus wesentlich flacher verläuft, als
beim naiven Algorithmus. Somit zeigt auch dieses Beispiel, daß die Wahl des Algorithmus wesentlichen Einfluß auf das Laufzeit-Verhalten hat.
Parallele Versionen
Aufbauend auf dem Jama-Algorithmus wurden die Versionen mit den klassischen Threads, den
Councurrency Utilities lt. Java 5, und den zwei Varianten des Fork/Join-Frameworks programmiert.
Die Laufzeiten für parallele Abarbeitung sollen am Beispiel der Multiplikation zweier 1000×1000Matrizen in Abhängigkeit von der Anzahl der verwendeten Threads betrachtet werden.
Folgende Abkürzungen sollen gelten:
Th
Variante mit klassischen Threads
TP
ThreadPool-Variante mit Concurrency Utities lt. Java 5
FJ1
Fork/Join-Variante die von FJTask ableitet
FJ2
Fork/Join-Variante die von ForkJoinTask ableitet
Laufzeiten in ms
Anzahl an
Threads
Th
TP
FJ1
FJ2
1
3784
3799
3851
3805
2
1950
1956
1965
1975
4
1153
1156
1148
1161
8
915
918
808
1056
16
808
879
1055
895
32
857
895
883
911
64
733
731
769
771
128
633
768
799
784
256
592
977
792
784
512
597
1879
860
826
1024
659
6193
943
959
2048
757
28089
1232
1334
Performance-Messungen
49
Java-Technologien zur parallelen Abarbeitung im Vergleich
2500
Laufzeit in ms
2000
1500
Th
TP
FJ1
FJ2
1000
500
0
1
2
4
8
16
32
64
128
256
512
1024 2048
Anzahl an Threads
Erkenntnisse aus den Messdaten
Aus obigem Diagramm bzw. obigen Messwerten läßt sich ablesen, daß – bis zu einer vernünftigen
Anzahl an Threads von ca. 128 – alle vier Technologien ähnliche Laufzeiten haben. Bei einer
großen Anzahl an Threads versagt allein die ThreadPool-Varinate ihren Dienst durch extrem lange
Laufzeiten. In diesem Bereich erweisen sich die einfachen Java-Threads als erstaunlich konstant.
Im Gegensatz zur sequentiellen Berechnung mithilfe des Jama-Algorithmus, die 3700 ms gedauert
hat, ergeben sich für die 2. Fork/Join-Variante folgende Speedups. Die Werte zu den anderen
Technologien sind aufgrund ähnlicher Laufzeiten ähnlich.
Speedup
1
0,97
2
1,87
4
3,19
8
3,5
16
4,13
32
4,06
64
4,8
128
4,72
256
4,72
512
4,48
1024
3,86
2048
2,77
Speedups
6
5
4
Speedups
Threads
3
2
1
0
2
1
8
4
32
16
128
64
512
256
Anzahl an Threads
2048
1024
Performance-Messungen
50
4.6 Problemstellung: Jacobi-Relaxation
Im Gegensatz zu den bisher betrachteten Problemstellung unterscheidet sich diese dadurch, daß
zwischen den Threads ein Informations-Austausch erfolgen muß. Der Austausch erfolgt für jede Iteration, das heißt, die Iterationen müssen synchronisiert ablaufen. Für diese Synchronisation steht die
Klasse java.util.concurrent.CyclicBarrier zur Verfügung. Eine derartige Barriere
bewirkt, daß hier eine bestimmte Anzahl an Threads aufeinander warten. Im Gegensatz zu join() werden die Threads aber nicht beendet, sondern laufen weiter, sobald genügend Threads am BarrierPunkt angelangt sind. Sobald dies der Fall ist wird üblicherweise eine Aktion durchgeführt, in diesem Programm eben der Werte-Austausch der Ergebnisse der bisherigen Iterationen.
Im Konstruktor einer CyclicBarrier wird angegeben, wie viele Threads an der Barriere erwartet werden. Diese werden als Parties bezeichnet. In der run()-Methode erfolgt die Synchronisation
über die await()-Methode. Diese kann eine Exception auslösen, falls Threads vorzeitig beendet
werden. Der Begriff CyclicBarrier soll darauf hinweisen, daß die Barriere wiederholt verwendet werden kann.
Sequentielle Variante
Die sequentielle Abarbeitung (JacobiSeq.java) ergab folgende Meßdaten:
Sequentielle Berechnung (Jacobi-Matrix)
20000
18000
16000
14000
Laufzeit in ms
Iterationen Laufzeit/[ms]
­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
2 376 4 724 6 1068 8 1417 10 1768 15 2625 20 3493 25 4350 30 5216 35 6084
40 6959 45 7815
50 8682 60 10427
70 12168
80 13868
90 15563
100 17290
­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
12000
10000
8000
6000
4000
2000
0
10
20
30
40
50
60
Iterationen
70
80
90 100
Performance-Messungen
51
Parallele Variante
Die parallele Berechnung (JacobiThreadPool.java) ergab folgende Meßdaten:
Parallele Berechnung (Jacobi-Relaxation)
2500
2000
Laufzeit in ms
Iterationen Laufzeit/[ms]
­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
10 294 ­ 352 20 483 ­ 621 30 781 ­ 936
40 764 ­ 1328
50 1117 ­ 1304 60 1269 ­ 1586 70 1458 ­ 1836 80 1705 ­ 1873
90 1826 ­ 2152
100 1853 ­ 2322 ­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
1500
1000
500
0
10
20
30
40
50
60
70
80
90
100
Iterationen
Aus den Mittelwerten der parallelen Laufzeiten in Relation zu den sequentiellen Laufzeiten errechnen sich folgende Speedups:
Speedups (Jacobi-Relaxation)
9
8
7
6
Speedup
Iterationen Speedups
­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
10 5,47 20 6,33 30 6,08
40 6,65
50 7,18 60 7,31 70 7,38 80 7,75
90 7,82
100 8,28 ­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
5
4
3
2
1
0
10
20
30
40
50
60
70
80
90
100
Iterationen
Die Anzahl der Iterationen kann über ein Kommandozeilen-Argument angegeben werden. Für jede
Iteration wird ein eigener Thread angelegt.
Performance-Messungen
52
Erkenntnisse aus den Messdaten
Die Messdaten der sequentiellen Berechnung weisen nur eine sehr geringe Streuung auf. Die Laufzeiten sind praktisch (linear) proportional zur Anzahl der durchlaufenen Iterationen. Bei der parallelen Berechnung sind starke Streuungen zu beobachten. Die Streuungen wurden – wie in den vorhergehenden Problemstellungen – aus 12 Messwerten ermittelt. Die Daten zeigen, daß die parallele
Abarbeitung offensichtlich wesentlich effizienter erfolgt. Die Laufzeiten steigen auch hier fast linear zur Zahl der Iterationen an. Allerdings sind die Laufzeiten der parallelen Berechnung wesentlich besser, als die Laufzeiten der sequentiellen Berechnung. Dadurch ergeben sich erfreuliche
Speedups, wie man aus obiger Tabelle bzw. obigem Diagramm sehen kann.
Zusammenfassung
53
5 Zusammenfassung
Neuere Entwicklungen bezüglich paralleler Programmierung – wie das Fork/Join-Framework – zeigen, daß es durchaus möglich ist, in reinem Java-Code skalierbare, effiziente und portierbare parallele Algorithmen zu schreiben. Java ist eine innovative Programmiersprache, in der versucht wird,
dem Programmierer immer effizientere und komfortablere Frameworks zur Verfügung zu stellen.
Das wird mit zunehmender Notwendigkeit des parallelen Programmierens auch nötig sein, damit
die Sprache weiterhin akzeptiert wird.
Die Experimente der Laufzeit-Messung haben gezeigt, daß durch die Verwendung von Technologien zur parallelen Programm-Abarbeitung nicht automatisch eine Verbesserung der Laufzeit eintritt. In einigen Fällen kommt es sogar zu einer deutlichen Verschlechterung durch den Mehraufwand der Erzeugung, Verwaltung und Entfernung von Threads bzw. Thread-Pools. Eine Verschlechterung der Laufzeit hängt sicher oft damit zusammen, daß der benutzte Algorithmus nicht gut zur
parallelen Verarbeitung geeignet ist. Daher sind geeignete (spezielle) parallele Algorithmen mindestens ebenso wichtig, wie Frameworks für die parallele Berechnung, um durch die Verwendung von
MultiCore-Prozessoren bessere Performance zu erreichen. Die Entwicklung dieser parallelen Algorithmen bedarf allerdings guter Kenntnisse der jeweiligen Problemstellung, und zählt damit sicher
nicht zu den einfachen Aufgaben in der Informatik. Aus diesem Grund wird die parallele Programmierung – mit Hilfe von MultiCore-Prozessoren – in der Informatik ein interessantes Kapitel bleiben, das die Informatiker noch längere Zeit beschäftigen wird, und sicherlich noch viel Interessantes hervorbringt.
Anhang A (Programme zur Problemstellung „Numerische Integration“)
Anhang A (Programme zur Problemstellung „Numerische
Integration“)
PiCalcSeq.java
import java.util.Scanner;
/**
*
Klasse zur Berechnung der Zahl pi durch numerische Integration.
*
Die Berechnung erfolgt durch einen sequentiellen Algorithmus.
*/
public class PiCalcSeq {
public static void main(String[] args) {
int nx = getStripes(args);
// nx ... Anzahl der Abschnitte auf der x-Achse
long startTime = System.currentTimeMillis();
// Beginn der Laufzeit-Messung
double dx = 1 / (double) nx;
double pi = 0;
for (int i = 0; i < nx; i++) {
for (int ii = 0; ii < 1000000;
double x = (i + 0.5) * dx;
pi += 4 / (1 + x * x);
}
}
pi *= dx;
}
// dx ... IntervallBreite
ii++) {
// x ... Intervall-MittelPunkt
long endTime = System.currentTimeMillis();
// Ende der Laufzeit-Messung
System.out.println(" Berechnete Zahl pi: " + (pi / 1E06));
System.out.println(" RechenZeit: " + (endTime - startTime) + " ms");
/** Anzahl der Teil-Intervalle festlegen.
* @param
args KommandozeilenArgumente
* @return Anzahl der Teil-Intervalle
*/
private static int getStripes(String[] args) {
int ret = -1;
try {
if (args.length > 0) {
ret = new Integer(args[0]).intValue();
} else {
System.out.println(" Die Zahl pi wird mit Hilfe einer numerischen Integration");
System.out.println(" auf dem Intervall [0, 1] berechnet.");
System.out.print(" In wie viele Teile soll das Intervall geteilt werden: ");
Scanner sc = new Scanner(System.in);
ret = sc.nextInt();
sc.close();
}
} catch (Exception e) {
System.err.println(" Ungültige Eingabe!");
System.exit(2);
}
return ret;
}
}
54
Anhang A (Programme zur Problemstellung „Numerische Integration“)
PiCalcThreads1.java
import java.util.Scanner;
/**
*
Klasse zur Berechnung der Zahl pi durch numerische Integration.
*
Die Berechnung erfolgt parallel durch Java-Threads.
*/
public class PiCalcThreads1 {
/** Anzahl der
private static
/** Anzahl der
private static
verfuegbaren Rechen-Einheiten (CPUs) */
int nodes;
Abschnitte auf der x-Achse
*/
int nx;
public static void main(String[] args) {
nodes = getNodes(args);
nx = getStripes(args);
System.out.println(" " + nodes + " Nodes
" + nx + " TeilIntervalle");
long startTime = System.currentTimeMillis();
// Beginn der Laufzeit-Messung
PiCalcThreads1 piCalc = new PiCalcThreads1();
double pi = piCalc.calc();
}
long endTime = System.currentTimeMillis();
// Ende der Laufzeit-Messung
System.out.println(" Berechnete Zahl pi: " + (pi / 1E06));
System.out.println(" RechenZeit: " + (endTime - startTime) + " ms");
/** Pi berechnen.
* pi als Näherung einer numerischer Integration
*/
private double calc() {
Service[] service = new Service[nodes];
Thread[] serverThread = new Thread[nodes];
int n1 = nx / nodes;
// max. Anzahl Abschnitte, die 1 Node zu berechnen hat
if (nx % nodes > 0)
n1++;
// Ausführungs-Einheiten festlegen:
for (int i = 0; i < nodes; i++) {
service[i] = new Service(n1);
serverThread[i] = new Thread(service[i]);
}
// Problem in TeilProbleme (Tasks) zerlegen:
double dx = 1 / (double) nx;
// dx ... IntervallBreite
for (int i = 0; i < nx; i++) {
double x = (i + 0.5) * dx;
// x ... Intervall-MittelPunkt
service[i % nodes].addValue(x);
}
// Teil-Probleme von den Ausführungs-Einheiten parallel abarbeiten lassen:
for (int i = 0; i < nodes; i++) {
serverThread[i].start();
}
// Ergebnisse der Teil-Probleme synchronisieren:
try {
for (int i = 0; i < nodes; i++) {
serverThread[i].join();
}
} catch (InterruptedException e) {}
// Teil-Ergebnisse zusammenfügen:
double pi = 0;
for (int i = 0; i < nodes; i++) {
pi += service[i].getResult();
}
}
pi *= dx;
return pi;
55
Anhang A (Programme zur Problemstellung „Numerische Integration“)
/** Anzahl der Rechen-Einheiten festlegen.
* @param s ArgumentListe
* @return Anzahl der Rechen-Einheiten
*/
public static int getNodes(String[] s) {
int ret = -1;
try {
switch (s.length) {
case 0:
System.out.println(" Usage:
java PiCalcThreads1 [<#TeilIntervalle>] <#CPUs>");
System.exit(1);
break;
case 1:
ret = new Integer(s[0]).intValue();
break;
default:
ret = new Integer(s[1]).intValue();
}
} catch (Exception e) {
System.err.println(" Ungültiges Argument!");
System.exit(1);
}
return ret;
}
/** Anzahl der Teil-Intervalle festlegen.
* @param
args KommandozeilenArgumente
* @return Anzahl der Teil-Intervalle
*/
private static int getStripes(String[] args) {
int ret = -1;
try {
if (args.length > 1) {
ret = new Integer(args[0]).intValue();
} else {
System.out.println(" Die Zahl pi wird mit Hilfe einer numerischen Integration");
System.out.println(" auf dem Intervall [0, 1] berechnet.");
System.out.print(" In wie viele Teile soll das Intervall geteilt werden: ");
Scanner sc = new Scanner(System.in);
ret = sc.nextInt();
sc.close();
}
} catch (Exception e) {
System.err.println(" Ungültige Eingabe!");
System.exit(2);
}
return ret;
}
/**
*
Klasse zur Berechnung und Verwaltung von Teil-Ergebnissen
*/
class Service implements Runnable
{
/** Berechnetes Teil-Ergebnis dieser Instanz */
private double result;
/** Zu bearbeitende x-Werte */
private double xvals[];
/** Zaehler der bereits uebergebenen x-Werte */
private int cnt;
/** Anzahl der zu bearbeitenden x-Werte */
private int n1;
56
Anhang A (Programme zur Problemstellung „Numerische Integration“)
/** Konstruktor
* @param n1 Anzahl der Stellen, zu denen die FunktionsWerte zu berechnen sind.
*/
public Service(int n1) {
result = 0;
xvals = new double[n1];
cnt = 0;
this.n1 = n1;
}
/** Stelle an der Abszisse speichern, zu der der FunktionsWert zu berechnen ist.
* @param x Stelle an der Abszisse
*/
public void addValue(double x) {
if (cnt < n1) {
xvals[cnt] = x;
cnt++;
}
}
/** TeilErgebnis dieser Instanz abfragen. */
public double getResult() {
return result;
}
}
}
@Override
public void run() {
for (int i = 0; i < xvals.length; i++) {
for (int ii = 0; ii < 1000000; ii++) {
double x = xvals[i];
if (x > 0) {
// 0 bedeutet unbelegt (letztes Element kann unbenutzt sein!)
result += 4 / (1 + x * x);
}
}
}
}
57
Anhang A (Programme zur Problemstellung „Numerische Integration“)
PiCalcThreads2.java
import java.util.Scanner;
/** Klasse zur Berechnung der Zahl pi durch numerische Integration. */
public class PiCalcThreads2
{
/** Anzahl der verfuegbaren Rechen-Einheiten (CPUs) */
private static int nodes;
/** Anzahl der Abschnitte auf der x-Achse
private static int nx;
*/
public static void main(String[] args) {
nodes = getNodes(args);
nx = getStripes(args);
System.out.println(" " + nodes + " Nodes
" + nx + " TeilIntervalle");
long startTime = System.currentTimeMillis();
PiCalcThreads2 piCalc = new PiCalcThreads2();
double pi = piCalc.calc();
}
long endTime = System.currentTimeMillis();
System.out.println("Berechnete Zahl pi: " + (pi / 1E06));
System.out.println("RechenZeit: " + (endTime - startTime) + " ms");
/** Pi berechnen.
* pi als Näherung einer numerischer Integration
*/
private double calc() {
Service[] service = new Service[nodes];
Thread[] serverThread = new Thread[nodes];
double dx = 1 / (double) nx;
int n1 = nx / nodes;
if (nx % nodes > 0)
n1++;
// dx ... IntervallBreite
// max. Anzahl Abschnitte, die 1 Node zu berechnen hat
for (int i = 0; i < nodes; i++) {
service[i] = new Service(i, nodes, n1, dx);
serverThread[i] = new Thread(service[i]);
}
for (int i = 0; i < nodes; i++) {
serverThread[i].start();
}
try {
for (int i = 0; i < nodes; i++) {
serverThread[i].join();
}
} catch (InterruptedException e) {}
double pi = 0;
for (int i = 0; i < nodes; i++) {
pi += service[i].getResult();
}
}
pi *= dx;
return pi;
58
Anhang A (Programme zur Problemstellung „Numerische Integration“)
/** Anzahl der Rechen-Einheiten festlegen.
* @param s ArgumentListe
* @return Anzahl der Rechen-Einheiten
*/
public static int getNodes(String[] s) {
int ret = -1;
try {
switch (s.length) {
case 0:
System.out.println(" Usage:
java PiCalcThreads2 [<#TeilIntervalle>] <#CPUs>");
System.exit(1);
break;
case 1:
ret = new Integer(s[0]).intValue();
break;
default:
ret = new Integer(s[1]).intValue();
}
} catch (Exception e) {
System.err.println(" Ungültiges Argument!");
System.exit(1);
}
return ret;
}
/** Anzahl der Teil-Intervalle festlegen.
* @param
args KommandozeilenArgumente
* @return Anzahl der Teil-Intervalle
*/
private static int getStripes(String[] args) {
int ret = -1;
try {
if (args.length > 1) {
ret = new Integer(args[0]).intValue();
} else {
System.out.println(" Die Zahl pi wird mit Hilfe einer numerischen Integration");
System.out.println(" auf dem Intervall [0, 1] berechnet.");
System.out.print(" In wie viele Teile soll das Intervall geteilt werden: ");
Scanner sc = new Scanner(System.in);
ret = sc.nextInt();
sc.close();
}
} catch (Exception e) {
System.err.println(" Ungültige Eingabe!");
System.exit(2);
}
return ret;
}
/** Klasse zur Berechnung und Verwaltung von Teil-Ergebnissen */
class Service implements Runnable
{
/** eindeutige Nummer der Instanz */
private int n;
/** Anzahl der verfuegbaren Rechen-Einheiten (CPUs) */
private int nodes;
/** Anzahl der zu bearbeitenden x-Werte */
private int n1;
/** Intervall-Breite */
private double dx;
/** Berechnetes Teil-Ergebnis dieser Instanz */
private double result;
59
Anhang A (Programme zur Problemstellung „Numerische Integration“)
/** Konstruktor
* @param n1 Anzahl der Stellen, zu denen die FunktionsWerte zu berechnen sind.
*/
public Service(int n, int nodes, int n1, double dx) {
this.n = n;
this.nodes = nodes;
this.n1 = n1;
this.dx = dx;
this.result = 0;
}
/** TeilErgebnis dieser Instanz abfragen. */
public double getResult() {
return result;
}
@Override
public void run() {
int n= this.n;
for (int i = 0; i < n1; i++) {
for (int ii = 0; ii < 1000000; ii++) {
double x = (n + i * nodes + 0.5) * dx;
if (x < 1) {
result += 4 / (1 + x * x);
}
}
}
}
}
}
// x ... Intervall-MittelPunkt
// Intervall [0, 1]
60
Anhang A (Programme zur Problemstellung „Numerische Integration“)
PiCalcThreadPool.java
import
import
import
import
import
import
import
import
java.util.Scanner;
java.util.concurrent.Executors;
java.util.concurrent.ExecutorService;
java.util.concurrent.Callable;
java.util.concurrent.Future;
java.util.ArrayList;
java.util.List;
java.util.Iterator;
/**
*
Klasse zur Berechnung der Zahl pi durch numerische Integration.
*
Die Berechnung erfolgt parallel durch die Java-Concurrency-Utilities von Java 1.5.
*/
public class PiCalcThreadPool
{
/** Anzahl der verfuegbaren Rechen-Einheiten (CPUs) */
private static int nodes;
/** Anzahl der Abschnitte auf der x-Achse */
private static int nx;
public static void main(String[] args) {
nodes = getNodes(args);
nx = getStripes(args);
long startTime = System.currentTimeMillis();
// Beginn der Laufzeit-Messung
PiCalcThreadPool piCalc = new PiCalcThreadPool();
double pi = piCalc.calc();
}
long endTime = System.currentTimeMillis();
// Ende der Laufzeit-Messung
if (pi > 0) {
System.out.println(" Berechnete Zahl pi: " + (pi / 1E06));
System.out.println(" RechenZeit: " + (endTime - startTime) + " ms");
}
/** Pi berechnen.
* @return pi als Näherung einer numerischer Integration
*/
private double calc() {
ExecutorService executor = Executors.newFixedThreadPool(nodes);
List<Future> futures = new ArrayList<Future>(nx);
try {
double dx = 1 / (double) nx;
// dx ... IntervallBreite
for (int i = 0; i < nx; i++) {
double x = (i + 0.5) * dx;
// x ... Intervall-MittelPunkt
futures.add(executor.submit(new Service(x)));
// System.out.println("hier 1");
}
double pi = 0;
for (Iterator<Future> it = futures.iterator(); it.hasNext(); ) {
pi += ((Double) it.next().get()).doubleValue();
}
pi *= dx;
return pi;
}
} catch (Exception e) {
System.err.println(" Fehler in der Berechnung!");
return -1;
} finally {
executor.shutdown();
}
61
Anhang A (Programme zur Problemstellung „Numerische Integration“)
/** Anzahl der Rechen-Einheiten festlegen.
* @param s ArgumentListe
* @return Anzahl der Rechen-Einheiten
*/
public static int getNodes(String[] s) {
int ret = -1;
try {
switch (s.length) {
case 0:
System.out.println(" Usage:
java PiCalcThreadPool
System.exit(1);
break;
case 1:
ret = new Integer(s[0]).intValue();
break;
default:
ret = new Integer(s[1]).intValue();
}
} catch (Exception e) {
System.err.println(" Ungültiges Argument!");
System.exit(1);
}
return ret;
}
[<#TeilIntervalle>] <#CPUs>");
/** Anzahl der Teil-Intervalle festlegen.
* @param
args KommandozeilenArgumente
* @return Anzahl der Teil-Intervalle
*/
private static int getStripes(String[] args) {
int ret = -1;
try {
if (args.length > 1) {
ret = new Integer(args[0]).intValue();
} else {
System.out.println(" Die Zahl pi wird mit Hilfe einer numerischen Integration");
System.out.println(" auf dem Intervall [0, 1] berechnet.");
System.out.print(" In wie viele Teile soll das Intervall geteilt werden: ");
Scanner sc = new Scanner(System.in);
ret = sc.nextInt();
sc.close();
}
} catch (Exception e) {
System.err.println(" Ungültige Eingabe!");
System.exit(2);
}
return ret;
}
/** Innere Klasse zur Berechnung und Verwaltung von Teil-Ergebnissen */
class Service implements Callable<Double>
{
private double x;
// zu bearbeitender x-Wert
/** Konstruktor
* @param x Stelle, zu der der FunktionsWert zu berechnen ist.
*/
public Service(double x) {
// System.out.println("hier 2");
this.x = x;
}
}
}
@Override
public Double call() throws Exception {
// System.out.println("hier 3");
double result = 0;
for (int ii = 0; ii < 1000000; ii++) {
result += 4 / (1 + x * x);
}
return new Double( result );
}
62
Anhang A (Programme zur Problemstellung „Numerische Integration“)
PiCalcFJ1.java
import
import
import
import
import
java.util.Scanner;
java.util.ArrayList;
java.util.List;
java.util.Iterator;
EDU.oswego.cs.dl.util.concurrent.*;
/**
*
Klasse zur Berechnung der Zahl pi durch numerische Integration.
*
Die Berechnung erfolgt parallel durch das Fork-Join-Framework
*
durch Ableitung von der Klasse FJTask.
*/
public class PiCalcFJ1 extends FJTask
{
private static int nx;
// Anzahl d. Teilintervalle
private double x;
// Stelle des Teil-Intervalls an der Abszisse
private volatile double result;
// Ergebnis für das Teil-Intervall
private boolean isMaster = false;
// Master-Thread?
/** Konstruktor */
public PiCalcFJ1(boolean isMaster) {
this.isMaster = isMaster;
}
/** Konstruktor */
public PiCalcFJ1(double x) {
this.x = x;
}
@Override
public void run() {
if (this.isMaster) {
PiCalcFJ1[] tasks = new PiCalcFJ1[nx];
double dx = 1 / (double) nx;
// dx ... IntervallBreite
// Problem in TeilProbleme (Tasks) zerlegen:
for (int i = 0; i < nx; i++) {
double x = (i + 0.5) * dx;
// x ... Intervall-MittelPunkt
tasks[i] = new PiCalcFJ1(x);
}
// Tasks zur parallelen Abarbeitung übergeben:
coInvoke(tasks);
// Teil-Ergebnisse synchronisieren:
this.result = 0;
for (int i = 0; i < nx; i++) {
this.result += tasks[i].getAnswer();
}
this.result *= dx;
this.result /= 1E06;
} else {
// Berechnung der Slaves (Worker-Threads):
for (int ii = 0; ii < 1000000; ii++) {
this.result += 4 / (1 + this.x * this.x);
}
}
}
public double getAnswer() {
if (!isDone())
throw new IllegalStateException("
return this.result;
}
Noch nicht berechnet!");
63
Anhang A (Programme zur Problemstellung „Numerische Integration“)
public static void main(String[] args) {
try {
int nodes = getNodes(args);
nx = getStripes(args);
long startTime = System.currentTimeMillis();
// Anzahl d. verfuegbaren CPUs
// Beginn der Laufzeit-Messung
FJTaskRunnerGroup group = new FJTaskRunnerGroup(nodes);
PiCalcFJ1 master = new PiCalcFJ1(true);
group.invoke(master);
double pi = master.getAnswer();
}
long endTime = System.currentTimeMillis();
// Ende der Laufzeit-Messung
System.out.println(" Berechnete Zahl pi: " + pi);
System.out.println(" RechenZeit: " + (endTime - startTime) + " ms");
} catch (InterruptedException ex) {}
/** Anzahl der Rechen-Einheiten festlegen.
* @param s ArgumentListe
* @return Anzahl der Rechen-Einheiten
*/
public static int getNodes(String[] s) {
int ret = -1;
try {
switch (s.length) {
case 0:
System.out.println(" Usage:
java PiCalcFJ1 [<#TeilIntervalle>] <#CPUs>");
System.exit(1);
break;
case 1:
ret = new Integer(s[0]).intValue();
break;
default:
ret = new Integer(s[1]).intValue();
}
} catch (Exception e) {
System.err.println(" Ungültiges Argument!");
System.exit(1);
}
return ret;
}
/** Anzahl der Teil-Intervalle festlegen.
* @param
args KommandozeilenArgumente
* @return Anzahl der Teil-Intervalle
*/
private static int getStripes(String[] args) {
int ret = -1;
try {
if (args.length > 1) {
ret = new Integer(args[0]).intValue();
} else {
System.out.println(" Die Zahl pi wird mit Hilfe einer numerischen Integration");
System.out.println(" auf dem Intervall [0, 1] berechnet.");
System.out.print(" In wie viele Teile soll das Intervall geteilt werden: ");
Scanner sc = new Scanner(System.in);
ret = sc.nextInt();
sc.close();
}
} catch (Exception e) {
System.err.println(" Ungültige Eingabe!");
System.exit(2);
}
return ret;
}
}
64
Anhang A (Programme zur Problemstellung „Numerische Integration“)
PiCalcFJ2.java
import
import
import
import
import
import
java.util.Scanner;
java.util.ArrayList;
java.util.List;
java.util.Iterator;
EDU.oswego.cs.dl.util.concurrent.*;
jsr166y.*;
/**
*
Klasse zur Berechnung der Zahl pi durch numerische Integration.
*
Die Berechnung erfolgt parallel durch das Fork-Join-Framework
*
durch Ableitung von der Klasse RecursiveAction, die wiederum von
*
ForkJoinTask abgeleitet ist.
*/
public class PiCalcFJ2 extends RecursiveAction
{
private static int nx;
// Anzahl d. Teilintervalle
private double x;
// Stelle des Teil-Intervalls an der Abszisse
private volatile double result;
// Ergebnis für das Teil-Intervall
private boolean isMaster = false;
// Master-Thread?
/** Konstruktor */
public PiCalcFJ2(boolean isMaster) {
this.isMaster = isMaster;
}
/** Konstruktor
*
@param x
Stelle des Teil-Intervalls
*/
public PiCalcFJ2(double x) {
this.x = x;
}
/** Berechnung laut Formel */
@Override
public void compute() {
if (this.isMaster) {
PiCalcFJ2[] tasks = new PiCalcFJ2[nx];
double dx = 1 / (double) nx;
// dx ... IntervallBreite
// Problem in TeilProbleme (Tasks) zerlegen:
for (int i = 0; i < nx; i++) {
double x = (i + 0.5) * dx;
// x ... Intervall-MittelPunkt
tasks[i] = new PiCalcFJ2(x);
}
// Tasks zur parallelen Abarbeitung übergeben:
invokeAll(tasks);
// Teil-Ergebnisse synchronisieren:
this.result = 0;
for (int i = 0; i < nx; i++) {
this.result += tasks[i].getAnswer();
}
this.result *= dx;
this.result /= 1E06;
} else {
// Berechnung der Slaves (Worker-Threads):
for (int ii = 0; ii < 1000000; ii++) {
this.result += 4 / (1 + this.x * this.x);
}
}
}
/** Prueft, ob die Berechnung fertig ist. */
public double getAnswer() {
if (!isDone())
throw new IllegalStateException("Not yet computed");
return this.result;
}
65
Anhang A (Programme zur Problemstellung „Numerische Integration“)
public static void main(String[] args) {
int nodes = getNodes(args);
nx = getStripes(args);
long startTime = System.currentTimeMillis();
// Anzahl d. verfuegbaren CPUs
// Anzahl d. Teilintervalle
// Beginn der Laufzeit-Messung
ForkJoinPool pool = new ForkJoinPool(nodes);
PiCalcFJ2 master = new PiCalcFJ2(true);
pool.invoke(master);
double pi = master.getAnswer();
}
long endTime = System.currentTimeMillis();
// Ende der Laufzeit-Messung
System.out.println(" Berechnete Zahl pi: " + pi);
System.out.println(" RechenZeit: " + (endTime - startTime) + " ms");
/** Anzahl der Rechen-Einheiten festlegen.
* @param s ArgumentListe
* @return Anzahl der Rechen-Einheiten
*/
public static int getNodes(String[] s) {
int ret = -1;
try {
switch (s.length) {
case 0:
System.out.println(" Usage:
java PiCalcFJ2 [<#TeilIntervalle>] <#CPUs>");
System.exit(1);
break;
case 1:
ret = new Integer(s[0]).intValue();
break;
default:
ret = new Integer(s[1]).intValue();
}
} catch (Exception e) {
System.err.println(" Ungültiges Argument!");
System.exit(1);
}
return ret;
}
/** Anzahl der Teil-Intervalle festlegen.
* @param
args KommandozeilenArgumente
* @return Anzahl der Teil-Intervalle
*/
private static int getStripes(String[] args) {
int ret = -1;
try {
if (args.length > 1) {
ret = new Integer(args[0]).intValue();
} else {
System.out.println(" Die Zahl pi wird mit Hilfe einer numerischen Integration");
System.out.println(" auf dem Intervall [0, 1] berechnet.");
System.out.print(" In wie viele Teile soll das Intervall geteilt werden: ");
Scanner sc = new Scanner(System.in);
ret = sc.nextInt();
sc.close();
}
} catch (Exception e) {
System.err.println(" Ungültige Eingabe!");
System.exit(2);
}
return ret;
}
}
66
Anhang B (Programme zur Problemstellung „Sortieren“)
Anhang B (Programme zur Problemstellung „Sortieren“)
QuickSortSeq.java
/**
* Klasse zum Sortieren eines double-Arrays mittels Quicksort-Algorithmus
*/
public class QuickSortSeq {
public static void main(String[] args) {
try {
int size = 0;
// Groesse des Arrays, das sortiert wird
if (args.length > 0) {
size = Integer.parseInt(args[0]);
} else {
System.out.println(" Usage:
java QuickSortSeq <n> ... (n = size of array)");
System.exit(-1);
}
double[] v = new double[size];
initArray(v);
long startTime = System.currentTimeMillis();
// Beginn der Laufzeit-Messung
sort(v, 0, v.length-1);
long endTime = System.currentTimeMillis();
// Ende der Laufzeit-Messung
//
printArray(v);
System.out.println(" Elapsed time: " + (endTime - startTime) + " ms");
} catch (NumberFormatException e) {
System.err.println(" Ungültige, nicht numerische Argumente!");
System.exit(-1);
}
}
/** Initialisierung des Arrays mit zufälligen double-Werten
* @param v initialsiertes Array
*/
public static void initArray(double[] v) {
for (int i=0; i < v.length; i++) {
v[i] = Math.random();
}
}
/** Array ausgeben
* @param v auszugebendes Array
*/
public static void printArray(double[] v) {
for (int i=0; i < v.length; i++) {
System.out.println(" " + v[i]);
}
System.out.println();
}
/** Array sortieren
* @param a zu sortierendes Array
* @param l linker Rand des bearbeiteten Bereichs
* @param r rechter Rand des bearbeiteten Bereichs
*/
public static void sort(double[] a, int l, int r){
int i = 0; int j = 0;
double x = 0; double h = 0;
i = l; j = r;
x = a[(l+r)/2];
67
Anhang B (Programme zur Problemstellung „Sortieren“)
}
}
do {
while (a[i] < x)
while (x < a[j])
if (i <= j) {
h = a[i];
a[i] = a[j];
a[j] = h;
i++; j--;
}
} while (i <= j);
if (l < j) sort(a,
if (i < r) sort(a,
{ i++; }
{ j--; }
l, j);
i, r);
68
Anhang B (Programme zur Problemstellung „Sortieren“)
QuickSortThreads.java
/*
*/
Quelle:
http://webcache.googleusercontent.com/search?q=cache:A6qfrK1YSNwJ:www.wiasberlin.de/people/telschow/2001ss-edv2/Vorlesungen/01-Parallelprogrammierung/01Parallelprogrammierung.ppt+parallel+quicksort&cd=9&hl=de&ct=clnk&gl=at
Beschreibung:
QuickSort mittels Threads
Ein Array aus zufälligen Werten wird erzeugt.
Die Größe des Arrays kann über einen Parameter angegeben werden.
Damit nicht zuviele kleine Threads erzeugt werden - die zuviel Overhead erzeugen wird über den Parameter c festgelegt, wann sequentiell weitergearbeitet wird.
/**
* Klasse zum Sortieren eines double-Arrays mittels Quicksort-Algorithmus
*/
public class QuickSortThreads extends Thread {
private
private
private
private
double[] a;
int l;
int r;
int c;
//
//
//
//
zu sortierendes Array
linker Rand des bearbeiteten Bereichs
rechter Rand des bearbeiteten Bereichs
MinimalGröße eines TeilArrays ab wann ein Thread erzeugt wird
/** Konstruktor
* @param a zu sortierendes Array
* @param l linker Rand des bearbeiteten Bereichs
* @param r rechter Rand des bearbeiteten Bereichs
* @param c Threshold ab dem parallel gearbeitet wird
*/
public QuickSortThreads(double[] a, int l, int r, int c) {
this.a = a;
this.l = l;
this.r = r;
this.c = c;
}
/** Konstruktor
* @param a zu sortierendes Array
*/
public QuickSortThreads(double[] a) {
this(a, 0, a.length - 1, a.length);
}
@Override
public void run() {
sort(a, l, r, c);
}
public static void main(String[] args) {
try {
int nthreads = 16;
// Anzahl an Worker-Threads
int size = 0;
// Groesse des Arrays, das sortiert wird
if (args.length > 0) {
size = Integer.parseInt(args[0]);
} else {
System.out.println(" Usage:
java QuickSortThreads <n> [threads]
... (n = size of array)");
System.exit(-1);
}
if (args.length > 1) {
nthreads = Integer.parseInt(args[1]);
}
69
Anhang B (Programme zur Problemstellung „Sortieren“)
//
}
double[] v = new double[size];
initArray(v);
long startTime = System.currentTimeMillis();
sort(v, 0, v.length-1, nthreads);
long endTime = System.currentTimeMillis();
printArray(v);
System.out.println(" Elapsed time: " + (endTime
} catch (NumberFormatException e) {
System.err.println(" Ungültige, nicht numerische
System.exit(-1);
}
// Beginn der Laufzeit-Messung
// Ende der Laufzeit-Messung
- startTime) + " ms");
Argumente!");
/** Initialisierung des Arrays mit zufälligen double-Werten
* @param v initialsiertes Array
*/
public static void initArray(double[] v) {
for (int i=0; i < v.length; i++) {
v[i] = Math.random();
}
}
/** Array ausgeben
* @param v auszugebendes Array
*/
public static void printArray(double[] v) {
for (int i=0; i < v.length; i++) {
System.out.println(" " + v[i]);
}
System.out.println();
}
}
/** Array sortieren
* @param a zu sortierendes Array
* @param l linker Rand des bearbeiteten Bereichs
* @param r rechter Rand des bearbeiteten Bereichs
* @param c Threshold ab dem parallel gearbeitet wird
*/
public static void sort(double[] a, int l, int r, int c) {
int i = 0; int j = 0;
double x = 0; double h = 0;
i = l; j = r;
x = a[(l+r)/2];
do {
while (a[i] < x) { i++; }
while (x < a[j]) { j--; }
if (i <= j) {
h = a[i];
a[i] = a[j];
a[j] = h;
i++; j--;
}
} while (i <= j);
QuickSortThreads tl = null;
QuickSortThreads tr = null;
if ((l+c < j) && (i+c < r)) {
// parallele Abarbeitung:
tl = new QuickSortThreads(a, l, j, c);
tr = new QuickSortThreads(a, i, r, c);
tl.start();
tr.start();
} else {
// sequentielle Abarbeitung:
if (l < j)
sort(a, l, j, c);
if (i < r)
sort(a, i, r, c);
}
try {
if (tl != null) tl.join();
if (tr != null) tr.join();
} catch (InterruptedException ie) {}
}
70
Anhang B (Programme zur Problemstellung „Sortieren“)
QuickSortThreadPool.java
/*
*/
Quelle:
http://webcache.googleusercontent.com/search?q=cache:A6qfrK1YSNwJ:www.wiasberlin.de/people/telschow/2001ss-edv2/Vorlesungen/01-Parallelprogrammierung/01Parallelprogrammierung.ppt+parallel+quicksort&cd=9&hl=de&ct=clnk&gl=at
Beschreibung:
QuickSort mittels Threads
Ein Array aus zufälligen Werten wird erzeugt.
Die Größe des Arrays kann über einen Parameter angegeben werden.
Damit nicht zuviele kleine Threads erzeugt werden - die zuviel Overhead erzeugen wird über den Parameter c festgelegt, wann sequentiell weitergearbeitet wird.
import java.util.concurrent.*;
/**
* Klasse zum Sortieren eines double-Arrays mittels Quicksort-Algorithmus
*/
public class QuickSortThreadPool implements Runnable {
private
private
private
private
private
static ExecutorService exec;
double[] a;
// zu sortierendes Array
int l;
// linker Rand des bearbeiteten Bereichs
int r;
// rechter Rand des bearbeiteten Bereichs
int c;
// MinimalGröße eines TeilArrays ab wann ein Thread erzeugt wird
/** Konstruktor
* @param a zu sortierendes Array
* @param l linker Rand des bearbeiteten Bereichs
* @param r rechter Rand des bearbeiteten Bereichs
* @param c Threshold ab dem parallel gearbeitet wird
*/
public QuickSortThreadPool(double[] a, int l, int r, int c) {
this.a = a;
this.l = l;
this.r = r;
this.c = c;
}
/** Konstruktor
* @param a zu sortierendes Array
*/
public QuickSortThreadPool(double[] a) {
this(a, 0, a.length - 1, a.length);
}
@Override
public void run() {
sort(a, l, r, c);
}
public static void main(String[] args) {
try {
int nthreads = 1;
// Anzahl an Worker-Threads
int size = 0;
// Groesse des Arrays, das sortiert wird
if (args.length > 0) {
size = Integer.parseInt(args[0]);
} else {
System.out.println(" Usage:
java QuickSortThreadPool <n> [threads]
... (n = size of array)");
System.exit(-1);
}
71
Anhang B (Programme zur Problemstellung „Sortieren“)
if (args.length > 1) {
nthreads = Integer.parseInt(args[1]);
}
//
}
double[] v = new double[size];
initArray(v);
long startTime = System.currentTimeMillis();
// Beginn der Laufzeit-Messung
exec = Executors.newFixedThreadPool(nthreads);
sort(v, 0, v.length-1, v.length);
long endTime = System.currentTimeMillis();
// Ende der Laufzeit-Messung
printArray(v);
System.out.println(" Elapsed time: " + (endTime - startTime) + " ms");
} catch (NumberFormatException e) {
System.err.println(" Ungültige, nicht numerische Argumente!");
System.exit(-1);
}
/** Initialisierung des Arrays mit zufälligen double-Werten
* @param v initialsiertes Array
*/
public static void initArray(double[] v) {
for (int i=0; i < v.length; i++) {
v[i] = Math.random();
}
}
/** Array ausgeben
* @param v auszugebendes Array
*/
public static void printArray(double[] v) {
for (int i=0; i < v.length; i++) {
System.out.println(" " + v[i]);
}
System.out.println();
}
/** Array sortieren
* @param a zu sortierendes Array
* @param l linker Rand des bearbeiteten Bereichs
* @param r rechter Rand des bearbeiteten Bereichs
* @param c Threshold ab dem parallel gearbeitet wird
*/
public static void sort(double[] a, int l, int r, int c) {
int i = 0; int j = 0;
double x = 0; double h = 0;
i = l; j = r;
x = a[(l+r)/2];
do {
while (a[i] < x) { i++; }
while (x < a[j]) { j--; }
if (i <= j) {
h = a[i];
a[i] = a[j];
a[j] = h;
i++; j--;
}
} while (i <= j);
if ((l+c < j) && (i+c < r)) {
// parallele Abarbeitung:
Runnable taskL = new QuickSortThreadPool(a, l, l+c, c);
Runnable taskR = new QuickSortThreadPool(a, i, i+c, c);
Future futureL = exec.submit(taskL);
Future futureR = exec.submit(taskR);
try {
futureL.get();
futureR.get();
} catch (InterruptedException e) {
System.err.println("Task unterbrochen!");
72
Anhang B (Programme zur Problemstellung „Sortieren“)
}
}
} catch (ExecutionException e) {
System.err.println("Fehler bei der Ausführung des Tasks!");
}
} else {
// sequentielle Abarbeitung:
if (l < j)
sort(a, l, j, c);
if (i < r)
sort(a, i, r, c);
}
73
Anhang B (Programme zur Problemstellung „Sortieren“)
74
QuickSortFJ1.java
import EDU.oswego.cs.dl.util.concurrent.*;
/**
* Klasse zum Sortieren eines double-Arrays mittels Quicksort-Algorithmus
*/
class QuickSortFJ1 extends FJTask {
private
private
private
private
double[] a;
int l;
int r;
int c;
// zu sortierendes Array
// linker Rand des bearbeiteten Bereichs
// rechter Rand des bearbeiteten Bereichs
// MinimalGröße eines TeilArrays ab wann ein Thread erzeugt wird
/** Konstruktor
* @param a zu sortierendes Array
* @param l linker Rand des bearbeiteten Bereichs
* @param r rechter Rand des bearbeiteten Bereichs
* @param c Threshold ab dem parallel gearbeitet wird
*/
public QuickSortFJ1(double[] a, int l, int r, int c) {
this.a = a;
this.l = l;
this.r = r;
this.c = c;
}
/** Konstruktor
* @param a zu sortierendes Array
*/
public QuickSortFJ1(double[] a) {
this(a, 0, a.length - 1, a.length);
}
@Override
public void run() {
sort(a, l, r, c);
}
/** Kontrolle, ob die Berechnung fertig ist.
* @return true, wenn das Ergebnis vorliegt, sonst FehlerMeldung
*/
public boolean getAnswer() {
if (!isDone())
throw new IllegalStateException("Not yet computed");
return true;
}
public static void main(String[] args) {
try {
int groupSize = 1;
// Anzahl an Worker-Threads
int size = 0;
// Groesse des Arrays, das sortiert wird
if (args.length > 0) {
size = Integer.parseInt(args[0]);
} else {
System.out.println(" Usage:
java QuickSortFJ1 <n> [threads] ... (n = size of array)");
System.exit(-1);
}
if (args.length > 1) {
groupSize = Integer.parseInt(args[1]);
}
double[] v = new double[size];
initArray(v);
Anhang B (Programme zur Problemstellung „Sortieren“)
//
}
long startTime = System.currentTimeMillis();
// Beginn der Laufzeit-Messung
FJTaskRunnerGroup group = new FJTaskRunnerGroup(groupSize);
QuickSortFJ1 f = new QuickSortFJ1(v);
group.invoke(f);
f.getAnswer();
long endTime = System.currentTimeMillis();
// Ende der Laufzeit-Messung
printArray(v);
System.out.println(" Elapsed Time: " + (endTime - startTime) + " ms");
} catch (NumberFormatException e) {
System.err.println(" Ungültige, nicht numerische Argumente!");
System.exit(-1);
} catch (InterruptedException ex) {} // die
/** Initialisierung des Arrays mit zufälligen double-Werten
* @param v initialsiertes Array
*/
public static void initArray(double[] v) {
for (int i=0; i < v.length; i++) {
v[i] = Math.random();
}
}
/** Array ausgeben
* @param v auszugebendes Array
*/
public static void printArray(double[] v) {
for (int i=0; i < v.length; i++) {
System.out.println(" " + v[i]);
}
System.out.println();
}
/** Array sortieren
* @param a zu sortierendes Array
* @param l linker Rand des bearbeiteten Bereichs
* @param r rechter Rand des bearbeiteten Bereichs
* @param c Threshold ab dem parallel gearbeitet wird
*/
public static void sort(double[] a, int l, int r, int c) {
int i = l;
int j = r;
double h = 0;
double x = a[ (l+r) / 2 ];
do {
while (a[i] < x) { i++; }
while (x < a[j]) { j--; }
if (i <= j) {
h = a[i];
a[i] = a[j];
a[j] = h;
i++; j--;
}
} while (i <= j);
}
}
if ((l+c < j) && (i+c < r)) {
// parallele Abarbeitung:
QuickSortFJ1 taskL = new QuickSortFJ1(a, l, l+c, c);
QuickSortFJ1 taskR = new QuickSortFJ1(a, i, i+c, c);
coInvoke(taskL, taskR);
taskL.getAnswer();
taskR.getAnswer();
} else {
// sequentielle Abarbeitung:
if (l < j)
sort(a, l, j, c);
if (i < r)
sort(a, i, r, c);
}
75
Anhang B (Programme zur Problemstellung „Sortieren“)
76
QuickSortFJ2.java
/*
*/
Variante mit der Klasse "ForkJoinTask" (statt FJTask)
import EDU.oswego.cs.dl.util.concurrent.*;
import jsr166y.*;
/**
* Klasse zum Sortieren eines double-Arrays mittels Quicksort-Algorithmus
*/
class QuickSortFJ2 extends RecursiveAction {
private
private
private
private
double[] a;
int l;
int r;
int c;
// zu sortierendes Array
// linker Rand des bearbeiteten Bereichs
// rechter Rand des bearbeiteten Bereichs
// MinimalGröße eines TeilArrays ab wann ein Thread erzeugt wird
/** Konstruktor
* @param a zu sortierendes Array
* @param l linker Rand des bearbeiteten Bereichs
* @param r rechter Rand des bearbeiteten Bereichs
* @param c Threshold ab dem parallel gearbeitet wird
*/
public QuickSortFJ2(double[] a, int l, int r, int c) {
this.a = a;
this.l = l;
this.r = r;
this.c = c;
}
/** Konstruktor
* @param a zu sortierendes Array
*/
public QuickSortFJ2(double[] a) {
this(a, 0, a.length - 1, a.length);
}
@Override
public void compute() {
sort(a, l, r, c);
}
/** Kontrolle, ob die Berechnung fertig ist.
* @return true, wenn das Ergebnis vorliegt, sonst FehlerMeldung
*/
public boolean getAnswer() {
if (!isDone())
throw new IllegalStateException("Not yet computed");
return true;
}
public static void main(String[] args) {
try {
int groupSize = 1;
// Anzahl an Worker-Threads
int size = 0;
// Groesse des Arrays, das sortiert wird
if (args.length > 0) {
size = Integer.parseInt(args[0]);
} else {
System.out.println(" Usage:
java QuickSortFJ2 <n> [threads] ... (n = size of array)");
System.exit(-1);
}
if (args.length > 1) {
groupSize = Integer.parseInt(args[1]);
}
Anhang B (Programme zur Problemstellung „Sortieren“)
//
}
double[] v = new double[size];
initArray(v);
long startTime = System.currentTimeMillis();
ForkJoinPool pool = new ForkJoinPool(groupSize);
QuickSortFJ2 f = new QuickSortFJ2(v);
pool.invoke(f);
f.getAnswer();
long endTime = System.currentTimeMillis();
printArray(v);
System.out.println(" Elapsed Time: " + (endTime
} catch (NumberFormatException e) {
System.err.println(" Ungültige, nicht numerische
System.exit(-1);
}
// Beginn der Laufzeit-Messung
// Ende der Laufzeit-Messung
- startTime) + " ms");
Argumente!");
/** Initialisierung des Arrays mit zufälligen double-Werten
* @param v initialsiertes Array
*/
public static void initArray(double[] v) {
for (int i=0; i < v.length; i++) {
v[i] = Math.random();
}
}
/** Array ausgeben
* @param v auszugebendes Array
*/
public static void printArray(double[] v) {
for (int i=0; i < v.length; i++) {
System.out.println(" " + v[i]);
}
System.out.println();
}
/** Array sortieren
* @param a zu sortierendes Array
* @param l linker Rand des bearbeiteten Bereichs
* @param r rechter Rand des bearbeiteten Bereichs
* @param c Threshold ab dem parallel gearbeitet wird
*/
public static void sort(double[] a, int l, int r, int c) {
int i = l;
int j = r;
double h = 0;
double x = a[ (l+r) / 2 ];
do {
while (a[i] < x) { i++; }
while (x < a[j]) { j--; }
if (i <= j) {
h = a[i];
a[i] = a[j];
a[j] = h;
i++; j--;
}
} while (i <= j);
}
}
if ((l+c < j) && (i+c < r)) {
// parallele Abarbeitung:
QuickSortFJ2 taskL = new QuickSortFJ2(a, l, l+c, c);
QuickSortFJ2 taskR = new QuickSortFJ2(a, i, i+c, c);
invokeAll(taskL, taskR);
taskL.getAnswer();
taskR.getAnswer();
} else {
// sequentielle Abarbeitung:
if (l < j)
sort(a, l, j, c);
if (i < r)
sort(a, i, r, c);
}
77
Anhang C (Programme zur Problemstellung „Fibonacci-Zahlen“)
Anhang C (Programme zur Problemstellung „FibonacciZahlen“)
FibSeq.java
/*
Beispiele von Fibonnaci-Zahlen auf:
http://www.ijon.de/mathe/fibonacci/index.html
*/
/**
*
Klasse zur Berechnung einer Fibonacci-Zahl.
*
Die Berechnung erfolgt durch einen sequentiellen Algorithmus.
*/
public class FibSeq {
/** Berechnung einer Fibonacci-Zahl durch Rekursion.
* @param n OrdnungsZahl der Fibonacci-Zahl
* @return
berechnete Fibonacci-Zahl */
private static int seqFib(int n) {
if (n <= 1)
return n;
else
return seqFib(n-1) + seqFib(n-2);
}
public static void main(String[] args) {
int num = 0;
if (args.length == 1) {
num = Integer.parseInt(args[0]);
} else {
System.out.println("Usage:
java FibSeq <n>
System.exit(-1);
}
}
}
// compute fib(num)
... (computes fib(n))");
long startTime = System.currentTimeMillis();
// Beginn der Laufzeit-Messung
int result = seqFib(num);
long endTime = System.currentTimeMillis();
// Ende der Laufzeit-Messung
System.out.println(" fib(" + num + ") = " + result);
System.out.println(" Elapsed Time: " + (endTime - startTime) + " ms");
78
Anhang C (Programme zur Problemstellung „Fibonacci-Zahlen“)
79
FibThreads.java
/*
*/
Beispiele von Fibonnaci-Zahlen auf:
http://www.ijon.de/mathe/fibonacci/index.html
/**
*
Klasse zur Berechnung einer Fibonacci-Zahl.
*
Die Abarbeitung erfolgt über Java-Threads.
*/
class FibThreads extends Thread {
static final int sequentialThreshold = 13;
volatile int number;
// for tuning
// argument/result
/** Konstruktor
* @param n OrdnungsZahl der Fibonacci-Zahl in der Reihe der Fibonacci-Zahlen
FibThreads(int n) {
number = n;
}
*/
/** Berechnung einer Fibonacci-Zahl durch Rekursion.
* @param n OrdnungsZahl der Fibonacci-Zahl
* @return
berechnete Fibonacci-Zahl */
int seqFib(int n) {
if (n <= 1)
return n;
else
return seqFib(n-1) + seqFib(n-2);
}
/** Abfrage des Ergebnisses.
* @return Fibonacci-Zahl des (Teil-)Problems
*/
int getAnswer() {
return number;
}
@Override
public void run() {
int n = number;
if (n <= sequentialThreshold)
number = seqFib(n);
else {
FibThreads f1 = new FibThreads(n - 1);
FibThreads f2 = new FibThreads(n - 2);
coInvoke(f1, f2);
number = f1.number + f2.number;
}
}
// BasisFall
// SubTasks erzeugen
// zur parallelen Ausführung übergeben
// Ergebnisse zusammenführen
/** Zwei Teil-Probleme zur parallelen Ausführung bringen,
* und anschließend synchronisieren.
* @param f1 1. Teil-Problem als Thread
* @param f2 2. Teil-Problem als Thread
*/
private void coInvoke(Thread f1, Thread f2) {
try {
f1.start();
f2.start();
f1.join();
f2.join();
} catch (InterruptedException ie) {
ie.printStackTrace();
}
}
Anhang C (Programme zur Problemstellung „Fibonacci-Zahlen“)
public static void main(String[] args) {
try {
int num = 0;
// compute fib(num)
if (args.length == 1) {
num = Integer.parseInt(args[0]);
} else {
System.out.println(" Usage:
java FibThreads <n> ... (computes fib(n))");
System.exit(-1);
}
long startTime = System.currentTimeMillis();
// Beginn der Laufzeit-Messung
FibThreads f = new FibThreads(num);
f.start();
f.join();
int result = f.getAnswer();
long endTime = System.currentTimeMillis();
// Ende der Laufzeit-Messung
System.out.println(" fib(" + num + ") = " + result);
System.out.println(" Elapsed Time: " + (endTime - startTime) + " ms");
}
}
}
catch (InterruptedException ex) {}
// die
80
Anhang C (Programme zur Problemstellung „Fibonacci-Zahlen“)
81
FibThreadPool.java
/*
Beispiele von Fibonnaci-Zahlen auf:
http://www.ijon.de/mathe/fibonacci/index.html
==============================
Variante mit Concurrency Utils
==============================
*/
In der main-Methode wird ein Thread nach der klassischen Thread-Programmierung gestartet.
Dieser Thread ruft allerdings in seiner run-Methode zwei weitere Threads auf.
Jeder dieser beiden Threads ruft wieder zwei weitere Tasks auf usw.
import java.util.concurrent.*;
/**
*
Klasse zur Berechnung einer Fibonacci-Zahl.
*
Die Abarbeitung erfolgt über die Concurrent-Utilities lt. Java 1.5.
*/
class FibThreadPool extends Thread{
static final int sequentialThreshold = 13;
// for tuning
volatile int number;
// argument/result
private static ExecutorService executor;
/** Konstruktor
* @param n OrdnungsZahl der Fibonacci-Zahl in der Reihe der Fibonacci-Zahlen
FibThreadPool(int n) {
number = n;
}
*/
/** Berechnung einer Fibonacci-Zahl durch Rekursion.
* @param n OrdnungsZahl der Fibonacci-Zahl
* @return
berechnete Fibonacci-Zahl */
int seqFib(int n) {
if (n <= 1)
return n;
else
return seqFib(n-1) + seqFib(n-2);
}
/** Abfrage des Ergebnisses.
* @return Fibonacci-Zahl des (Teil-)Problems
*/
int getResult() {
return number;
}
@Override
public void run() {
int n = number;
if (n <= sequentialThreshold)
// base case
number = seqFib(n);
else {
// create subtasks :
Callable<Integer> task1 = new Service(n-1);
Callable<Integer> task2 = new Service(n-2);
Future<Integer> future1 = executor.submit(task1);
Future<Integer> future2 = executor.submit(task2);
try {
// join both:
Integer result1 = future1.get();
Integer result2 = future2.get();
this.number = result1.intValue() + result2.intValue();
// combine results
Anhang C (Programme zur Problemstellung „Fibonacci-Zahlen“)
}
}
82
} catch (Exception e) {
System.err.println(" Fehler in der Berechnung!");
}
public static void main(String[] args) {
int num = 0;
int nodes = 16;
// number of threads
try {
if (args.length > 0) {
num = Integer.parseInt(args[0]);
} else {
System.out.println(" Usage:
java FibThreadPool <n> [threads]
System.exit(-1);
}
if (args.length > 1) {
nodes = Integer.parseInt(args[1]);
}
} catch (NumberFormatException e) {
System.err.println(" Ungültige, nicht numerische Argumente!");
System.exit(-1);
}
try {
long startTime = System.currentTimeMillis();
executor = Executors.newFixedThreadPool(nodes);
... (computes fib(n))");
// Beginn der Laufzeit-Messung
FibThreadPool f = new FibThreadPool(num);
f.start();
f.join();
int result = f.getResult();
}
long endTime = System.currentTimeMillis();
// Ende der Laufzeit-Messung
System.out.println(" fib(" + num + ") = " + result);
System.out.println(" Elapsed Time: " + (endTime - startTime) + " ms");
} catch (Exception e) {
System.err.println(" Fehler in der Berechnung!");
System.exit(-1);
} finally {
executor.shutdown();
}
/** Klasse zur Berechnung und Verwaltung von Teil-Ergebnissen */
class Service implements Callable<Integer>
{
private int n;
/** Konstruktor */
public Service(int n) {
this.n = n;
}
}
}
@Override
public Integer call() throws Exception {
FibThreadPool f = new FibThreadPool(this.n);
f.start();
f.join();
return
new Integer( f.getResult() );
}
Anhang C (Programme zur Problemstellung „Fibonacci-Zahlen“)
83
FibFJ1.java
import EDU.oswego.cs.dl.util.concurrent.*;
/**
*
*
*
*/
class
Klasse zur Berechnung einer Fibonacci-Zahl.
Die Abarbeitung erfolgt mit dem Fork/Join-Framework.
Die Klasse ist von FJTask abgeleitet.
FibFJ1 extends FJTask {
static final int sequentialThreshold = 13; // for tuning
volatile int number;
// argument/result
/** Konstruktor
* @param n OrdnungsZahl der Fibonacci-Zahl in der Reihe der Fibonacci-Zahlen
FibFJ1(int n) {
number = n;
}
*/
/** Berechnung einer Fibonacci-Zahl durch Rekursion.
* @param n OrdnungsZahl der Fibonacci-Zahl
* @return
berechnete Fibonacci-Zahl */
int seqFib(int n) {
if (n <= 1)
return n;
else
return seqFib(n-1) + seqFib(n-2);
}
/** Abfrage des Ergebnisses.
* @return Fibonacci-Zahl des (Teil-)Problems
*/
int getAnswer() {
if (!isDone())
throw new IllegalStateException("Not yet computed");
return number;
}
@Override
public void run() {
int n = number;
if (n <= sequentialThreshold)
// BasisFall
number = seqFib(n);
else {
// SubTasks erzeugen:
FibFJ1 f1 = new FibFJ1(n - 1);
FibFJ1 f2 = new FibFJ1(n - 2);
// Tasks parallel ausführen und synchronisieren:
coInvoke(f1, f2);
number = f1.number + f2.number;
}
}
public static void main(String[] args) {
try {
int groupSize = 1;
// Anzahl an Worker-Threads
int num = 0;
// Fibonacci-Zahl zum Argument <num> wird berechnet
if (args.length > 0) {
num = Integer.parseInt(args[0]);
} else {
System.out.println(" Usage:
java FibFJ1 <n> [threads] ... (computes fib(n))");
System.exit(-1);
}
if (args.length > 1) {
groupSize = Integer.parseInt(args[1]);
}
Anhang C (Programme zur Problemstellung „Fibonacci-Zahlen“)
}
}
long startTime = System.currentTimeMillis();
// Beginn der Laufzeit-Messung
FJTaskRunnerGroup group = new FJTaskRunnerGroup(groupSize);
FibFJ1 f = new FibFJ1(num);
group.invoke(f);
int result = f.getAnswer();
long endTime = System.currentTimeMillis();
// Ende der Laufzeit-Messung
System.out.println(" fib(" + num + ") = " + result);
System.out.println(" Elapsed Time: " + (endTime - startTime) + " ms");
} catch (NumberFormatException e) {
System.err.println(" Ungültige, nicht numerische Argumente!");
System.exit(-1);
} catch (InterruptedException ex) {}
84
Anhang C (Programme zur Problemstellung „Fibonacci-Zahlen“)
85
FibFJ2.java
/*
*/
Variante mit der Klasse "ForkJoinTask" (statt FJTask)
statt run-Methode => compute-Methode
statt FJTaskRunnerGroup -> ForkJoinPool
import EDU.oswego.cs.dl.util.concurrent.*;
import jsr166y.*;
/**
*
*
*
*
*/
class
Klasse zur Berechnung einer Fibonacci-Zahl.
Die Abarbeitung erfolgt mit dem Fork/Join-Framework.
Die Klasse ist von RecursiveTask abgeleitet,
die wiederum von ForkJoinTask abgeleitet ist.
FibFJ2 extends RecursiveTask {
static final int sequentialThreshold = 13; // for tuning
volatile int number;
// argument/result
/** Konstruktor
* @param n OrdnungsZahl der Fibonacci-Zahl in der Reihe der Fibonacci-Zahlen
FibFJ2(int n) {
number = n;
}
/** Berechnung einer Fibonacci-Zahl durch Rekursion.
* @param n OrdnungsZahl der Fibonacci-Zahl
* @return
berechnete Fibonacci-Zahl */
int seqFib(int n) {
if (n <= 1)
return n;
else
return seqFib(n-1) + seqFib(n-2);
}
/** Abfrage des Ergebnisses.
* @return Fibonacci-Zahl des (Teil-)Problems
*/
int getAnswer() {
if (!isDone())
throw new IllegalStateException("Not yet computed");
return number;
}
@Override
protected Integer compute() {
int n = number;
if (n <= sequentialThreshold)
// BasisFall
number = seqFib(n);
else {
// SubTasks erzeugen:
FibFJ2 f1 = new FibFJ2(n - 1);
FibFJ2 f2 = new FibFJ2(n - 2);
// Tasks parallel ausführen und synchronisieren:
invokeAll(f1, f2);
number = f1.number + f2.number;
}
return new Integer(number);
}
*/
Anhang C (Programme zur Problemstellung „Fibonacci-Zahlen“)
public static void main(String[] args) {
try {
int groupSize = 1;
// Anzahl an Worker-Threads
int num = 0;
// Fibonacci-Zahl zum Argument <num> wird berechnet
if (args.length > 0) {
num = Integer.parseInt(args[0]);
} else {
System.out.println(" Usage:
java FibFJ2 <n> [threads] ... (computes fib(n))");
System.exit(-1);
}
if (args.length > 1) {
groupSize = Integer.parseInt(args[1]);
}
}
}
long startTime = System.currentTimeMillis();
// Beginn der Laufzeit-Messung
ForkJoinPool pool = new ForkJoinPool(groupSize);
FibFJ2 f = new FibFJ2(num);
pool.invoke(f);
int result = f.getAnswer();
long endTime = System.currentTimeMillis();
// Ende der Laufzeit-Messung
System.out.println(" fib(" + num + ") = " + result);
System.out.println(" Elapsed Time: " + (endTime - startTime) + " ms");
} catch (NumberFormatException e) {
System.err.println(" Ungültige, nicht numerische Argumente!");
System.exit(-1);
}
86
Anhang D (Programme zur Problemstellung „Matrizen-Multiplikation“)
Anhang D (Programme zur Problemstellung „MatrizenMultiplikation“)
MatrixMult_naive.java
/*
Quelle:
http://artisans-serverintellect-com.si-eioswww6.com/default.asp?W40
http://forums.sun.com/thread.jspa?threadID=681520
*/
/**
*
Klasse zur Multiplikation zweier Matrizen.
*
Die Abarbeitung erfolgt sequentiell.
*/
public class MatrixMult_naive {
/* TestMatrizen:
static int[][] a = {{5,6,7},{4,8,9},{3,2,1}};
static int[][] b = {{6,4,8},{5,7,8},{4,3,2}};
static
static
*/
static
static
static
int[][] x = {{4,8,9},{3,2,1}};
int[][] y = {{6,4,2,0},{5,7,9,11},{4,3,2,1}};
int[][] a;
int[][] b;
int[][] c;
public static void main(String[] args) {
int rowsA = 1000, colsA = 1000;
int rowsB = 1000, colsB = 1000;
// prüfen, ob Dimensionen der Matrizen über Kommando-Zeilen-Par. angegeben:
if (args.length == 4) {
rowsA = new Integer(args[0]).intValue();
colsA = new Integer(args[1]).intValue();
rowsB = new Integer(args[2]).intValue();
colsB = new Integer(args[3]).intValue();
}
a = initMatrix(rowsA, colsA);
b = initMatrix(rowsB, colsB);
int rows = a.length;
int cols = b[0].length;
c = new int[rows][cols];
long startTime = System.currentTimeMillis();
naiveMatrixMultiply(a, b, c);
long endTime = System.currentTimeMillis();
/*
// Beginn der Laufzeit-Messung
// Ende der Laufzeit-Messung
printMatrix(a);
printMatrix(b);
printMatrix(c);
*/
System.out.println("
}
Matrix[" + a.length + "*" + a[0].length + "] * " +
"Matrix[" + b.length + "*" + b[0].length + "] = " +
"Matrix[" + rows + "*" + cols + "]");
System.out.println(" RechenZeit: " + (endTime - startTime) + " ms");
87
Anhang D (Programme zur Problemstellung „Matrizen-Multiplikation“)
/** Belegung einer Matrix mit zufälligen ganzzahligen Werten.
* @param r Anzahl der Zeilen der Matrix
* @param c Anzahl der Spalten der Matrix
* @return
Matrix mit ganzzahligen (zufälligen) Werten
*/
private static int[][] initMatrix(int r, int c) {
int[][] m = new int[r][c];
for (int i = 0; i < r; i++) {
for (int j = 0; j < c; j++) {
m[i][j] = (int) (Math.random() * 10);
}
}
return m;
}
/** Multiplikation der Matrizen:
*
c[m][p] = a[m][n] * b[n][p]
* @param a erste Matrize
* @param b zweite Matrize
* @param c Ergebnis-Matrix
*/
private static void naiveMatrixMultiply( final int[][] a,
final int[][] b,
final int[][] c ) {
check(a,b,c);
final int m = a.length;
final int n = b.length;
final int p = b[0].length;
for (int j = 0; j < p; j++) {
for (int i = 0; i < m; i++) {
int s = 0;
for (int k = 0; k < n; k++) {
s += a[i][k] * b[k][j];
}
c[i][j] = s;
}
}
}
/** Kompatibilität der Matrizen überprüfen
* (LaufzeitFehler im FehlerFall auslösen)
* @param a erste Matrize
* @param b zweite Matrize
* @param c Ergebnis-Matrix
*/
private static void check(final int[][] a,
final int[][] b,
final int[][] c) {
check(a);
check(b);
check(c);
if (c == a | c == b)
throw new IllegalArgumentException("a or b
// check dimensionality
final int am = a.length, an = a[0].length;
final int bm = b.length, bn = b[0].length;
final int cm = c.length, cn = c[0].length;
if (bm != an)
throw new IllegalArgumentException("a.n !=
"(Zweite Matrix muss soviele Zeilen haben,
if (cm != am)
throw new IllegalArgumentException("c.m !=
if (cn != bn)
throw new IllegalArgumentException("c.n !=
}
cannot be used for output c");
b.m " +
wie die erste Spalten hat!)");
a.m");
b.n");
88
Anhang D (Programme zur Problemstellung „Matrizen-Multiplikation“)
/** Prüfen, ob Matrizen leer sind
* (LaufzeitFehler im FehlerFall auslösen)
* @param array überprüfte Matrix
*/
private static void check(final int[][] array) {
if (array == null || array.length == 0 || array[0] == null)
throw new IllegalArgumentException("Array must be non-null and non empty.");
}
/** Matrix ausgeben
* @param a auszugebende Matrix
*/
private static void printMatrix(final int[][] a) {
final int rows = a.length;
final int cols = a[0].length;
}
}
System.out.println(rows + "*" + cols + "-Matrix:");
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
System.out.print(" " + a[i][j]);
}
System.out.println();
}
System.out.println();
89
Anhang D (Programme zur Problemstellung „Matrizen-Multiplikation“)
MatrixMult_jama.java
/*
Quelle:
http://artisans-serverintellect-com.si-eioswww6.com/default.asp?W40
http://forums.sun.com/thread.jspa?threadID=681520
*/
/**
*
Klasse zur Multiplikation zweier Matrizen.
*
Die Abarbeitung erfolgt sequentiell, jedoch
*
mit einem speziellen Algorithmus (Jama).
*/
public class MatrixMult_jama {
/* TestMatrizen:
static int[][] a = {{5,6,7},{4,8,9},{3,2,1}};
static int[][] b = {{6,4,8},{5,7,8},{4,3,2}};
static
static
*/
static
static
static
int[][] x = {{4,8,9},{3,2,1}};
int[][] y = {{6,4,2,0},{5,7,9,11},{4,3,2,1}};
int[][] a;
int[][] b;
int[][] c;
public static void main(String[] args) {
int rowsA = 1000, colsA = 1000;
int rowsB = 1000, colsB = 1000;
// prüfen, ob Dimensionen der Matrizen über Kommando-Zeilen-Par. angegeben:
if (args.length == 4) {
rowsA = new Integer(args[0]).intValue();
colsA = new Integer(args[1]).intValue();
rowsB = new Integer(args[2]).intValue();
colsB = new Integer(args[3]).intValue();
}
a = initMatrix(rowsA, colsA);
b = initMatrix(rowsB, colsB);
int rows = a.length;
int cols = b[0].length;
c = new int[rows][cols];
long startTime = System.currentTimeMillis();
jamaMatrixMultiply(a, b, c);
long endTime = System.currentTimeMillis();
/*
// Beginn der Laufzeit-Messung
// Ende der Laufzeit-Messung
printMatrix(a);
printMatrix(b);
printMatrix(c);
*/
System.out.println("
}
Matrix[" + a.length + "*" + a[0].length + "] * " +
"Matrix[" + b.length + "*" + b[0].length + "] = " +
"Matrix[" + rows + "*" + cols + "]");
System.out.println(" RechenZeit: " + (endTime - startTime) + " ms");
/** Belegung einer Matrix mit zufälligen ganzzahligen Werten.
* @param r Anzahl der Zeilen der Matrix
* @param c Anzahl der Spalten der Matrix
* @return
Matrix mit ganzzahligen (zufälligen) Werten
*/
private static int[][] initMatrix(int r, int c) {
int[][] m = new int[r][c];
for (int i = 0; i < r; i++) {
for (int j = 0; j < c; j++) {
m[i][j] = (int) (Math.random() * 10);
}
}
return m;
}
90
Anhang D (Programme zur Problemstellung „Matrizen-Multiplikation“)
/** Multiplikation der Matrizen:
*
c[m][p] = a[m][n] * b[n][p]
* @param a erste Matrize
* @param b zweite Matrize
* @param c Ergebnis-Matrix
*/
private static void jamaMatrixMultiply( final int[][] a,
final int[][] b,
final int[][] c) {
check(a,b,c);
final int m = a.length;
final int n = b.length;
final int p = b[0].length;
}
final int[] Bcolj = new int[n];
for (int j = 0; j < p; j++) {
for (int k = 0; k < n; k++) {
Bcolj[k] = b[k][j];
}
for (int i = 0; i < m; i++) {
final int[] Arowi = a[i];
int s = 0;
for (int k = 0; k < n; k++) {
s += Arowi[k] * Bcolj[k];
}
c[i][j] = s;
}
}
/** Kompatibilität der Matrizen überprüfen
* (LaufzeitFehler im FehlerFall auslösen)
* @param a erste Matrize
* @param b zweite Matrize
* @param c Ergebnis-Matrix
*/
private static void check(final int[][] a,
final int[][] b,
final int[][] c) {
check(a);
check(b);
check(c);
if (c == a | c == b)
throw new IllegalArgumentException("a or b
// check dimensionality
final int am = a.length, an = a[0].length;
final int bm = b.length, bn = b[0].length;
final int cm = c.length, cn = c[0].length;
if (bm != an)
throw new IllegalArgumentException("a.n !=
"(Zweite Matrix muss soviele Zeilen haben,
if (cm != am)
throw new IllegalArgumentException("c.m !=
if (cn != bn)
throw new IllegalArgumentException("c.n !=
}
cannot be used for output c");
b.m " +
wie die erste Spalten hat!)");
a.m");
b.n");
/** Prüfen, ob Matrizen leer sind
* (LaufzeitFehler im FehlerFall auslösen)
* @param array überprüfte Matrix
*/
private static void check(final int[][] array) {
if (array == null || array.length == 0 || array[0] == null)
throw new IllegalArgumentException("Array must be non-null and non empty.");
}
91
Anhang D (Programme zur Problemstellung „Matrizen-Multiplikation“)
/** Matrix ausgeben
* @param a auszugebende Matrix
*/
private static void printMatrix(final int[][] a) {
final int rows = a.length;
final int cols = a[0].length;
}
}
System.out.println(rows + "*" + cols + "-Matrix:");
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
System.out.print(" " + a[i][j]);
}
System.out.println();
}
System.out.println();
92
Anhang D (Programme zur Problemstellung „Matrizen-Multiplikation“)
MatrixMultThreads.java
/*
Quelle:
http://artisans-serverintellect-com.si-eioswww6.com/default.asp?W40
http://forums.sun.com/thread.jspa?threadID=681520
Beschreibung:
---------------------------------------------Variante mit klassischen Threads
---------------------------------------------Das Programm erzeugt 2 Matrizen mit zufälligen Werten. Die Dimension der beiden
Matrizen kann über die Argumente angegeben werden. Dann werden diese Matrizen
miteinander multipliziert.
*/
import java.util.*;
/**
*
Klasse zur Multiplikation zweier Matrizen.
*
Die Abarbeitung erfolgt mit Hilfe klassischer Threads.
*/
public class MatrixMultThreads {
/* TestMatrizen:
static int[][] a = {{5,6,7},{4,8,9},{3,2,1}};
static int[][] b = {{6,4,8},{5,7,8},{4,3,2}};
static
static
*/
static
static
static
int[][] x = {{4,8,9},{3,2,1}};
int[][] y = {{6,4,2,0},{5,7,9,11},{4,3,2,1}};
int[][] a;
int[][] b;
int[][] c;
public static void main(String[] args) {
int nThreads = 1;
// Anzahl der Threads (CPUs)
if (args.length > 0) {
nThreads = new Integer(args[0]).intValue();
} else {
System.out.println(" Aufruf:
java MatrixMultThreads <nThreads>" +
" [ <rowsA> <colsA> <rowsB> <colsB> ]");
System.exit(-1);
}
int rowsA = 1000, colsA = 1000;
int rowsB = 1000, colsB = 1000;
// prüfen, ob Dimensionen der Matrizen über Kommando-Zeilen-Par. angegeben:
if (args.length > 4) {
rowsA = new Integer(args[1]).intValue();
colsA = new Integer(args[2]).intValue();
rowsB = new Integer(args[3]).intValue();
colsB = new Integer(args[4]).intValue();
}
a = initMatrix(rowsA, colsA);
b = initMatrix(rowsB, colsB);
int rows = a.length;
int cols = b[0].length;
c = new int[rows][cols];
/*
*/
long startTime = System.currentTimeMillis();
threadedMatrixMultiply(a, b, c, nThreads);
long endTime = System.currentTimeMillis();
printMatrix(a);
printMatrix(b);
printMatrix(c);
// Beginn der Laufzeit-Messung
// Ende der Laufzeit-Messung
93
Anhang D (Programme zur Problemstellung „Matrizen-Multiplikation“)
System.out.println("
}
Matrix[" + a.length + "*" + a[0].length + "] * " +
"Matrix[" + b.length + "*" + b[0].length + "] = " +
"Matrix[" + rows + "*" + cols + "]");
System.out.println(" RechenZeit: " + (endTime - startTime) + " ms");
/** Belegung einer Matrix mit zufälligen ganzzahligen Werten.
* @param r Anzahl der Zeilen der Matrix
* @param c Anzahl der Spalten der Matrix
* @return
Matrix mit ganzzahligen (zufälligen) Werten
*/
private static int[][] initMatrix(int r, int c) {
int[][] m = new int[r][c];
for (int i = 0; i < r; i++) {
for (int j = 0; j < c; j++) {
m[i][j] = (int) (Math.random() * 10);
}
}
return m;
}
/** Multiplikation der Matrizen:
*
c[m][p] = a[m][n] * b[n][p]
* @param a erste Matrize
* @param b zweite Matrize
* @param c Ergebnis-Matrix
* @param numTasks Anzahl der benutzten Threads
*/
private static void threadedMatrixMultiply( final int[][] a,
final int[][] b,
final int[][] c, final int numTasks) {
check(a,b,c);
final ArrayList<Thread> threads = new ArrayList<Thread>(numTasks);
final int m = a.length;
final int n = b.length;
final int p = b[0].length;
for (int interval = numTasks, end = p, size = (int) Math.ceil(p * 1.0 / numTasks);
interval > 0;
interval--, end -= size) {
final int to = end;
final int from = Math.max(0, end - size);
final Runnable runnable = new Runnable() {
@Override
public void run() {
final int[] Bcolj = new int[n];
for (int j = from; j < to; j++) {
for (int k = 0; k < n; k++) {
Bcolj[k] = b[k][j];
}
for (int i = 0; i < m; i++) {
final int[] Arowi = a[i];
int s = 0;
for (int k = 0; k < n; k++) {
s += Arowi[k] * Bcolj[k];
}
c[i][j] = s;
}
}
}
};
Thread t = new Thread(runnable);
t.start();
threads.add(t);
}
try {
for (Iterator<Thread> it = threads.iterator(); it.hasNext(); ) {
it.next().join();
}
} catch (InterruptedException e) {}
}
94
Anhang D (Programme zur Problemstellung „Matrizen-Multiplikation“)
/** Kompatibilität der Matrizen überprüfen
* (LaufzeitFehler im FehlerFall auslösen)
* @param a erste Matrize
* @param b zweite Matrize
* @param c Ergebnis-Matrix
*/
private static void check(final int[][] a,
final int[][] b,
final int[][] c) {
check(a);
check(b);
check(c);
if (c == a | c == b)
throw new IllegalArgumentException("a or b
// check dimensionality
final int am = a.length, an = a[0].length;
final int bm = b.length, bn = b[0].length;
final int cm = c.length, cn = c[0].length;
if (bm != an)
throw new IllegalArgumentException("a.n !=
"(Zweite Matrix muss soviele Zeilen haben,
if (cm != am)
throw new IllegalArgumentException("c.m !=
if (cn != bn)
throw new IllegalArgumentException("c.n !=
}
cannot be used for output c");
b.m " +
wie die erste Spalten hat!)");
a.m");
b.n");
/** Prüfen, ob Matrizen leer sind
* (LaufzeitFehler im FehlerFall auslösen)
* @param array überprüfte Matrix
*/
private static void check(final int[][] array) {
if (array == null || array.length == 0 || array[0] == null)
throw new IllegalArgumentException("Array must be non-null and non empty.");
}
/** Matrix ausgeben
* @param a auszugebende Matrix
*/
private static void printMatrix(final int[][] a) {
final int rows = a.length;
final int cols = a[0].length;
}
}
System.out.println(rows + "*" + cols + "-Matrix:");
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
System.out.print(" " + a[i][j]);
}
System.out.println();
}
System.out.println();
95
Anhang D (Programme zur Problemstellung „Matrizen-Multiplikation“)
MatrixMultThreadPool.java
/*
Quelle:
http://artisans-serverintellect-com.si-eioswww6.com/default.asp?W40
http://forums.sun.com/thread.jspa?threadID=681520
Beschreibung:
---------------------------------------------Variante mit ExecutorService & FixedThreadPool
---------------------------------------------Das Programm erzeugt 2 Matrizen mit zufälligen Werten. Die Dimension der beiden
Matrizen kann über die Argumente angegeben werden. Dann werden diese Matrizen
miteinander multipliziert.
*/
import java.util.*;
import java.util.concurrent.*;
/**
*
Klasse zur Multiplikation zweier Matrizen.
*
Die Abarbeitung erfolgt mit Hilfe der Concurrency-Utils (lt. Java 1.5).
*/
public class MatrixMultThreadPool {
/* TestMatrizen:
static int[][] a = {{5,6,7},{4,8,9},{3,2,1}};
static int[][] b = {{6,4,8},{5,7,8},{4,3,2}};
static
static
*/
static
static
static
int[][] x = {{4,8,9},{3,2,1}};
int[][] y = {{6,4,2,0},{5,7,9,11},{4,3,2,1}};
int[][] a;
int[][] b;
int[][] c;
public static void main(String[] args) {
int nThreads = 1;
// Anzahl der Threads (CPUs)
if (args.length > 0) {
nThreads = new Integer(args[0]).intValue();
} else {
System.out.println(" Aufruf:
java MatrixMultThreadPool <nThreads>" +
" [ <rowsA> <colsA> <rowsB> <colsB> ]");
System.exit(-1);
}
int rowsA = 1000, colsA = 1000;
int rowsB = 1000, colsB = 1000;
// prüfen, ob Dimensionen der Matrizen über Kommando-Zeilen-Par. angegeben:
if (args.length > 4) {
rowsA = new Integer(args[1]).intValue();
colsA = new Integer(args[2]).intValue();
rowsB = new Integer(args[3]).intValue();
colsB = new Integer(args[4]).intValue();
}
a = initMatrix(rowsA, colsA);
b = initMatrix(rowsB, colsB);
int rows = a.length;
int cols = b[0].length;
c = new int[rows][cols];
/*
*/
long startTime = System.currentTimeMillis();
multiplyMatrix(a, b, c, nThreads);
long endTime = System.currentTimeMillis();
printMatrix(a);
printMatrix(b);
printMatrix(c);
// Beginn der Laufzeit-Messung
// Ende der Laufzeit-Messung
96
Anhang D (Programme zur Problemstellung „Matrizen-Multiplikation“)
System.out.println("
}
Matrix[" + a.length + "*" + a[0].length + "] * " +
"Matrix[" + b.length + "*" + b[0].length + "] = " +
"Matrix[" + rows + "*" + cols + "]");
System.out.println(" RechenZeit: " + (endTime - startTime) + " ms");
/** Belegung einer Matrix mit zufälligen ganzzahligen Werten.
* @param r Anzahl der Zeilen der Matrix
* @param c Anzahl der Spalten der Matrix
* @return
Matrix mit ganzzahligen (zufälligen) Werten
*/
private static int[][] initMatrix(int r, int c) {
int[][] m = new int[r][c];
for (int i = 0; i < r; i++) {
for (int j = 0; j < c; j++) {
m[i][j] = (int) (Math.random() * 10);
}
}
return m;
}
/** Multiplikation der Matrizen:
*
c[m][p] = a[m][n] * b[n][p]
* @param a erste Matrize
* @param b zweite Matrize
* @param c Ergebnis-Matrix
* @param numTasks Anzahl der benutzten Threads
*/
private static void multiplyMatrix( final int[][] a,
final int[][] b,
final int[][] c, final int numTasks) {
check(a,b,c);
final int m = a.length;
final int n = b.length;
final int p = b[0].length;
final ExecutorService executor = Executors.newFixedThreadPool(numTasks);
List<Future> futures = new ArrayList<Future>(numTasks);
for (int interval = numTasks, end = p, size = (int) Math.ceil(p * 1.0 / numTasks);
interval > 0;
interval--, end -= size) {
final int to = end;
final int from = Math.max(0, end - size);
final Runnable runnable = new Runnable() {
public void run() {
final int[] Bcolj = new int[n];
for (int j = from; j < to; j++) {
for (int k = 0; k < n; k++) {
Bcolj[k] = b[k][j];
}
for (int i = 0; i < m; i++) {
final int[] Arowi = a[i];
int s = 0;
for (int k = 0; k < n; k++) {
s += Arowi[k] * Bcolj[k];
}
c[i][j] = s;
}
}
}
};
futures.add(executor.submit(new Thread(runnable)));
}
try {
for (Iterator<Future> it = futures.iterator(); it.hasNext(); ) {
it.next().get();
}
executor.shutdown();
executor.awaitTermination(2, TimeUnit.DAYS); // O(n^3) can take a while!
} catch (Exception e) {}
}
97
Anhang D (Programme zur Problemstellung „Matrizen-Multiplikation“)
/** Kompatibilität der Matrizen überprüfen
* (LaufzeitFehler im FehlerFall auslösen)
* @param a erste Matrize
* @param b zweite Matrize
* @param c Ergebnis-Matrix
*/
private static void check(final int[][] a,
final int[][] b,
final int[][] c) {
check(a);
check(b);
check(c);
if (c == a | c == b)
throw new IllegalArgumentException("a or b
// check dimensionality
final int am = a.length, an = a[0].length;
final int bm = b.length, bn = b[0].length;
final int cm = c.length, cn = c[0].length;
if (bm != an)
throw new IllegalArgumentException("a.n !=
"(Zweite Matrix muss soviele Zeilen haben,
if (cm != am)
throw new IllegalArgumentException("c.m !=
if (cn != bn)
throw new IllegalArgumentException("c.n !=
}
cannot be used for output c");
b.m " +
wie die erste Spalten hat!)");
a.m");
b.n");
/** Prüfen, ob Matrizen leer sind
* (LaufzeitFehler im FehlerFall auslösen)
* @param array überprüfte Matrix
*/
private static void check(final int[][] array) {
if (array == null || array.length == 0 || array[0] == null)
throw new IllegalArgumentException("Array must be non-null and non empty.");
}
/** Matrix ausgeben
* @param a auszugebende Matrix
*/
private static void printMatrix(final int[][] a) {
final int rows = a.length;
final int cols = a[0].length;
}
}
System.out.println(rows + "*" + cols + "-Matrix:");
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
System.out.print(" " + a[i][j]);
}
System.out.println();
}
System.out.println();
98
Anhang D (Programme zur Problemstellung „Matrizen-Multiplikation“)
MatrixMultFJ1.java
import
import
import
import
java.util.ArrayList;
java.util.List;
java.util.Iterator;
EDU.oswego.cs.dl.util.concurrent.*;
/**
*
Klasse zur Multiplikation zweier Matrizen.
*
Die Abarbeitung erfolgt mit Hilfe des Fork/Join-Frameworks.
*
Die Klasse ist daher von FJTask abgeleitet.
*/
public class MatrixMultFJ1 extends FJTask {
/* TestMatrizen:
static int[][] a = {{5,6,7},{4,8,9},{3,2,1}};
static int[][] b = {{6,4,8},{5,7,8},{4,3,2}};
*/
static int[][] a = {{4,8,9},{3,2,1}};
static int[][] b = {{6,4,2,0},{5,7,9,11},{4,3,2,1}};
static int[][] a;
static int[][] b;
static int[][] c;
private
private
private
private
private
int[][] a_run;
int[][] b_run;
int[][] c_run;
int m, n;
int from, to;
static private int nThreads = 1;
private boolean isMaster = false;
// Anzahl der Threads (CPUs)
// Master-Thread?
/** Konstruktor */
public MatrixMultFJ1(boolean isMaster) {
this.isMaster = isMaster;
}
public MatrixMultFJ1(int[][] a_run, int[][] b_run, int[][] c_run,
int m, int n, int from, int to) {
this.a_run = a_run;
this.b_run = b_run;
this.c_run = c_run;
this.m = m;
this.n = n;
this.from = from;
this.to = to;
}
@Override
public void run() {
if (this.isMaster) {
check(a,b,c);
final int m = a.length;
final int n = b.length;
final int p = b[0].length;
MatrixMultFJ1[] tasks = new MatrixMultFJ1[nThreads];
for (int interval = nThreads, end = p, size = (int) Math.ceil(p * 1.0 / nThreads);
interval > 0;
interval--, end -= size) {
final int to = end;
final int from = Math.max(0, end - size);
MatrixMultFJ1 t = new MatrixMultFJ1(a, b, c, m, n, from, to);
tasks[interval-1] = t;
}
coInvoke(tasks);
99
Anhang D (Programme zur Problemstellung „Matrizen-Multiplikation“)
}
for (int i = nThreads; i > 0; i--) {
tasks[i-1].getAnswer();
}
} else {
final int[] Bcolj = new int[n];
for (int j = from; j < to; j++) {
for (int k = 0; k < n; k++) {
Bcolj[k] = b[k][j];
}
for (int i = 0; i < m; i++) {
final int[] Arowi = a[i];
int s = 0;
for (int k = 0; k < n; k++) {
s += Arowi[k] * Bcolj[k];
}
c[i][j] = s;
}
}
}
boolean getAnswer() {
if (!isDone())
throw new IllegalStateException("Not yet computed");
return true;
}
public static void main(String[] args) {
try {
if (args.length > 0) {
nThreads = new Integer(args[0]).intValue();
} else {
System.out.println(" Aufruf:
java MatrixMultFJ1 <nThreads>" +
" [ <rowsA> <colsA> <rowsB> <colsB> ]");
System.exit(-1);
}
int rowsA = 1000, colsA = 1000;
int rowsB = 1000, colsB = 1000;
// prüfen, ob Dimensionen der Matrizen über Kommando-Zeilen-Par. angegeben:
if (args.length > 4) {
rowsA = new Integer(args[1]).intValue();
colsA = new Integer(args[2]).intValue();
rowsB = new Integer(args[3]).intValue();
colsB = new Integer(args[4]).intValue();
}
a = initMatrix(rowsA, colsA);
b = initMatrix(rowsB, colsB);
int rows = a.length;
int cols = b[0].length;
c = new int[rows][cols];
/*
*/
}
long startTime = System.currentTimeMillis(); // Beginn der Laufzeit-Messung
final FJTaskRunnerGroup group = new FJTaskRunnerGroup(nThreads);
MatrixMultFJ1 master = new MatrixMultFJ1(true);
group.invoke(master);
master.getAnswer();
long endTime = System.currentTimeMillis();
// Ende der Laufzeit-Messung
printMatrix(a);
printMatrix(b);
printMatrix(c);
System.out.println("
Matrix[" + a.length + "*" + a[0].length + "] * " +
"Matrix[" + b.length + "*" + b[0].length + "] = " +
"Matrix[" + rows + "*" + cols + "]");
System.out.println(" RechenZeit: " + (endTime - startTime) + " ms");
} catch (InterruptedException ex) {}
100
Anhang D (Programme zur Problemstellung „Matrizen-Multiplikation“)
/** Belegung einer Matrix mit zufälligen ganzzahligen Werten.
* @param r Anzahl der Zeilen der Matrix
* @param c Anzahl der Spalten der Matrix
* @return
Matrix mit ganzzahligen (zufälligen) Werten
*/
private static int[][] initMatrix(int r, int c) {
int[][] m = new int[r][c];
for (int i = 0; i < r; i++) {
for (int j = 0; j < c; j++) {
m[i][j] = (int) (Math.random() * 10);
}
}
return m;
}
/** Kompatibilität der Matrizen überprüfen
* (LaufzeitFehler im FehlerFall auslösen)
* @param a erste Matrize
* @param b zweite Matrize
* @param c Ergebnis-Matrix
*/
private static void check(final int[][] a,
final int[][] b,
final int[][] c) {
check(a);
check(b);
check(c);
if (c == a | c == b)
throw new IllegalArgumentException("a or b
// check dimensionality
final int am = a.length, an = a[0].length;
final int bm = b.length, bn = b[0].length;
final int cm = c.length, cn = c[0].length;
if (bm != an)
throw new IllegalArgumentException("a.n !=
"(Zweite Matrix muss soviele Zeilen haben,
if (cm != am)
throw new IllegalArgumentException("c.m !=
if (cn != bn)
throw new IllegalArgumentException("c.n !=
}
cannot be used for output c");
b.m " +
wie die erste Spalten hat!)");
a.m");
b.n");
/** Prüfen, ob Matrizen leer sind
* (LaufzeitFehler im FehlerFall auslösen)
* @param array überprüfte Matrix
*/
private static void check(final int[][] array) {
if (array == null || array.length == 0 || array[0] == null)
throw new IllegalArgumentException("Array must be non-null and non empty.");
}
/** Matrix ausgeben
* @param a auszugebende Matrix
*/
private static void printMatrix(final int[][] a) {
final int rows = a.length;
final int cols = a[0].length;
}
}
System.out.println(rows + "*" + cols + "-Matrix:");
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
System.out.print(" " + a[i][j]);
}
System.out.println();
}
System.out.println();
101
Anhang D (Programme zur Problemstellung „Matrizen-Multiplikation“)
MatrixMultFJ2.java
import
import
import
import
import
java.util.ArrayList;
java.util.List;
java.util.Iterator;
EDU.oswego.cs.dl.util.concurrent.*;
jsr166y.*;
/**
*
Klasse zur Multiplikation zweier Matrizen.
*
Die Abarbeitung erfolgt mit Hilfe des Fork/Join-Frameworks.
*
Die Klasse ist von ForkJoinTask abgeleitet.
*/
public class MatrixMultFJ2 extends RecursiveAction {
/* TestMatrizen:
static int[][] a = {{5,6,7},{4,8,9},{3,2,1}};
static int[][] b = {{6,4,8},{5,7,8},{4,3,2}};
*/
static int[][] a = {{4,8,9},{3,2,1}};
static int[][] b = {{6,4,2,0},{5,7,9,11},{4,3,2,1}};
static int[][] a;
static int[][] b;
static int[][] c;
private
private
private
private
private
int[][] a_run;
int[][] b_run;
int[][] c_run;
int m, n;
int from, to;
static private int nThreads = 1;
private boolean isMaster = false;
// Anzahl der Threads (CPUs)
// Master-Thread?
/** Konstruktor */
public MatrixMultFJ2(boolean isMaster) {
this.isMaster = isMaster;
}
public MatrixMultFJ2(int[][] a_run, int[][] b_run, int[][] c_run,
int m, int n, int from, int to) {
this.a_run = a_run;
this.b_run = b_run;
this.c_run = c_run;
this.m = m;
this.n = n;
this.from = from;
this.to = to;
}
@Override
public void compute() {
if (this.isMaster) {
check(a,b,c);
final int m = a.length;
final int n = b.length;
final int p = b[0].length;
MatrixMultFJ2[] tasks = new MatrixMultFJ2[nThreads];
for (int interval = nThreads, end = p, size = (int) Math.ceil(p * 1.0 / nThreads);
interval > 0;
interval--, end -= size) {
final int to = end;
final int from = Math.max(0, end - size);
MatrixMultFJ2 t = new MatrixMultFJ2(a, b, c, m, n, from, to);
tasks[interval-1] = t;
}
102
Anhang D (Programme zur Problemstellung „Matrizen-Multiplikation“)
}
invokeAll(tasks);
for (int i = nThreads; i > 0; i--) {
tasks[i-1].getAnswer();
}
} else {
final int[] Bcolj = new int[n];
for (int j = from; j < to; j++) {
for (int k = 0; k < n; k++) {
Bcolj[k] = b[k][j];
}
for (int i = 0; i < m; i++) {
final int[] Arowi = a[i];
int s = 0;
for (int k = 0; k < n; k++) {
s += Arowi[k] * Bcolj[k];
}
c[i][j] = s;
}
}
}
boolean getAnswer() {
if (!isDone())
throw new IllegalStateException("Not yet computed");
return true;
}
public static void main(String[] args) {
if (args.length > 0) {
nThreads = new Integer(args[0]).intValue();
} else {
System.out.println(" Aufruf:
java MatrixMultFJ2 <nThreads>" +
" [ <rowsA> <colsA> <rowsB> <colsB> ]");
System.exit(-1);
}
int rowsA = 1000, colsA = 1000;
int rowsB = 1000, colsB = 1000;
// prüfen, ob Dimensionen der Matrizen über Kommando-Zeilen-Par. angegeben:
if (args.length > 4) {
rowsA = new Integer(args[1]).intValue();
colsA = new Integer(args[2]).intValue();
rowsB = new Integer(args[3]).intValue();
colsB = new Integer(args[4]).intValue();
}
a = initMatrix(rowsA, colsA);
b = initMatrix(rowsB, colsB);
int rows = a.length;
int cols = b[0].length;
c = new int[rows][cols];
long startTime = System.currentTimeMillis(); // Beginn der Laufzeit-Messung
final ForkJoinPool pool = new ForkJoinPool(nThreads);
MatrixMultFJ2 master = new MatrixMultFJ2(true);
pool.invoke(master);
master.getAnswer();
long endTime = System.currentTimeMillis();
// Ende der Laufzeit-Messung
/*
printMatrix(a);
printMatrix(b);
printMatrix(c);
*/
System.out.println("
}
Matrix[" + a.length + "*" + a[0].length + "] * " +
"Matrix[" + b.length + "*" + b[0].length + "] = " +
"Matrix[" + rows + "*" + cols + "]");
System.out.println(" RechenZeit: " + (endTime - startTime) + " ms");
103
Anhang D (Programme zur Problemstellung „Matrizen-Multiplikation“)
/** Belegung einer Matrix mit zufälligen ganzzahligen Werten.
* @param r Anzahl der Zeilen der Matrix
* @param c Anzahl der Spalten der Matrix
* @return
Matrix mit ganzzahligen (zufälligen) Werten
*/
private static int[][] initMatrix(int r, int c) {
int[][] m = new int[r][c];
for (int i = 0; i < r; i++) {
for (int j = 0; j < c; j++) {
m[i][j] = (int) (Math.random() * 10);
}
}
return m;
}
/** Kompatibilität der Matrizen überprüfen
* (LaufzeitFehler im FehlerFall auslösen)
* @param a erste Matrize
* @param b zweite Matrize
* @param c Ergebnis-Matrix
*/
private static void check(final int[][] a,
final int[][] b,
final int[][] c) {
check(a);
check(b);
check(c);
if (c == a | c == b)
throw new IllegalArgumentException("a or b
// check dimensionality
final int am = a.length, an = a[0].length;
final int bm = b.length, bn = b[0].length;
final int cm = c.length, cn = c[0].length;
if (bm != an)
throw new IllegalArgumentException("a.n !=
"(Zweite Matrix muss soviele Zeilen haben,
if (cm != am)
throw new IllegalArgumentException("c.m !=
if (cn != bn)
throw new IllegalArgumentException("c.n !=
}
cannot be used for output c");
b.m " +
wie die erste Spalten hat!)");
a.m");
b.n");
/** Prüfen, ob Matrizen leer sind
* (LaufzeitFehler im FehlerFall auslösen)
* @param array überprüfte Matrix
*/
private static void check(final int[][] array) {
if (array == null || array.length == 0 || array[0] == null)
throw new IllegalArgumentException("Array must be non-null and non empty.");
}
/** Matrix ausgeben
* @param a auszugebende Matrix
*/
private static void printMatrix(final int[][] a) {
final int rows = a.length;
final int cols = a[0].length;
}
}
System.out.println(rows + "*" + cols + "-Matrix:");
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
System.out.print(" " + a[i][j]);
}
System.out.println();
}
System.out.println();
104
Anhang E (Programme zur Problemstellung „Jacobi-Relaxation“)
Anhang E (Programme zur Problemstellung „JacobiRelaxation“)
JacobiSeq.java
public class JacobiSeq {
static
static
static
static
static
final int N = 1000;
final double OMEGA = 0.6;
double[][] f = new double[N][N];
double[][] u = new double[N][N];
double[][] uhelp = new double[N][N];
public static void main(String[] args) {
final int iterations;
if (args.length < 1) {
System.out.println(" Aufruf:
java JacobiSeq <iterations>");
System.exit(-1);
}
iterations = new Integer(args[0]).intValue();
//
}
init();
long startTime = System.currentTimeMillis(); // Beginn der Laufzeit-Messung
for (int i=0; i < iterations; i++) {
jacobi();
System.out.println((i+1) + ". Iteration");
}
long endTime = System.currentTimeMillis();
// Ende der Laufzeit-Messung
System.out.println(" RechenZeit: " + (endTime - startTime) + " ms");
private static void jacobi() {
for (int j=1; j < N-1; j++) {
for (int i=1; i < N-1; i++) {
uhelp[i][j] = (1-OMEGA) * u[i][j] +
OMEGA * 0.25 * ( f[i][j] + u[i-1][j]
+ u[i+1][j] + u[i][j+1] + u[i][j-1]);
}
}
}
for (int j=1; j < N-1; j++) {
for (int i=1; i < N-1; i++) {
u[i][j] = uhelp[i][j];
}
}
/** Initialisierung der rechten Seite der Rand- und Anfangs-Werte */
private static void init() {
for (int j=0; j < N; j++) {
for (int i=0; i < N; i++ ) {
f[i][j] = i * (i-1) + j * (j-1);
if (i==0 || i==N-1 || j==0 || j==N-1) {
// erste und letzte Elemente
u[i][j] = f[i][j];
} else {
u[i][j] = 1.0;
}
}
}
}
}
105
Anhang E (Programme zur Problemstellung „Jacobi-Relaxation“)
JacobiThreadPool.java
/* Variante mit Cyclic-Barrier und Thread-Pool auf Java 1.5
* jacobi-Methode wird in run-Methode implementiert
* Die meiste Zeit verbraucht das Schließen des Thread-Pools!
*/
import java.util.concurrent.*;
import java.util.*;
public class JacobiThreadPool implements Runnable {
final static int N = 1000;
final static double OMEGA = 0.6;
static double[][] f = new double[N][N];
static double[][] u = new double[N][N];
volatile static double[][] uhelp = new double[N][N];
static CyclicBarrier barrier;
public static void main(String[] args) {
try {
final int iterations;
if (args.length < 1) {
System.out.println(" Aufruf:
java JacobiThreadPool <iterations>");
System.exit(-1);
}
iterations = new Integer(args[0]).intValue();
init(f, u, N);
long startTime = System.currentTimeMillis();
// Beginn der Laufzeit-Messung
final ExecutorService executor = Executors.newFixedThreadPool(iterations);
List<Future> futures = new ArrayList<Future>(iterations);
//
barrier = new CyclicBarrier(iterations);
for (int i=0; i < iterations; i++) {
futures.add(executor.submit(new JacobiThreadPool()));
System.out.println((i+1) + ". Iteration");
}
for (Iterator<Future> it = futures.iterator(); it.hasNext(); ) {
it.next().get();
}
executor.shutdown();
executor.awaitTermination(2, TimeUnit.DAYS); // O(n^3) can take a while!
long endTime = System.currentTimeMillis();
}
// Ende der Laufzeit-Messung
System.out.println(" RechenZeit: " + (endTime - startTime) + " ms");
} catch (Exception e) {
System.err.println(" Fehler in der Berechnung!");
}
@Override
public void run() {
for (int j=1; j < N-1; j++) {
for (int i=1; i < N-1; i++) {
uhelp[i][j] = (1-OMEGA) * u[i][j] +
OMEGA * 0.25 * ( f[i][j] + u[i-1][j]
+ u[i+1][j] + u[i][j+1] + u[i][j-1]);
}
}
for (int j=1; j < N-1; j++) {
for (int i=1; i < N-1; i++) {
u[i][j] = uhelp[i][j];
}
}
106
Anhang E (Programme zur Problemstellung „Jacobi-Relaxation“)
}
try {
barrier.await();
}
catch (InterruptedException ex) { return; }
catch (BrokenBarrierException ex) { return; }
/** Initialisierung der rechten Seite der Rand- und Anfangs-Werte */
private static void init(double[][] f, double[][] u, final int N) {
for (int j=0; j < N; j++) {
for (int i=0; i < N; i++ ) {
f[i][j] = i * (i-1) + j * (j-1);
if (i==0 || i==N-1 || j==0 || j==N-1) {
// erste und letzte Elemente
u[i][j] = f[i][j];
} else {
u[i][j] = 1.0;
}
}
}
}
}
107
Literaturverzeichnis
108
6 Literaturverzeichnis
[1] Akhter, Shameem: Multicore-Programmierung, Verlag: entwickler.press, 2008
[2] Andrews, Gregory R.: Foundations of multithreaded, parallel, and distributed programming, Addison-Wesley, 2000
[3] Bell, Doug: Parallel programming, Wiley Heyden, 1983
[4] Bräunl, Thomas: Parallele Programmierung, Vieweg, 1993
[5] Brawer, Steven: Introduction to parallel programming, Academic Press, 1989
[6] Culler, David E.: Parallel computer architecture, Verlag: Kaufmann, 1999
[7] Feilmeier, M.: Parallele Datenverarbeitung und parallele Algorithmen, Kursmaterialien, 1979
[8] Goetz, Brian: Java concurrency in practice, Addison-Wesley, 2006
[9] Grama, Ananth: Introduction to parallel computing, Pearson Addison Wesley, 2007
[10] Kredel, Heinz: Thread- und Netzwerk-Programmierung mit Java, dpunkt-Verlag, 2002
[11] Lea, Douglas: Concurrent programming in Java, Addison-Wesley, 2005
[12] Lin, Calvin: Principles of parallel programming, Pearson, 2009
[13] Magee, Jeff: Concurrency, Wiley, 2006
[14] Malyshkin, Victor [Hrsg.]: Parallel computing technologies, Springer, 2003
[15] Naik, Vijay K.: Multiprocessing, Kluwer-Verlag, 1993
[16] Oechsle, Rainer: Parallele Programmierung mit Java Threads, Fachbuchverl. Leipzig im Carl-HanserVerl. 2001
[17] Petersen, Wesley P.: Introduction to parallel computing, Oxford Univ. Press, 2004
[18] Ragsdale, Susan [Hrsg.]: Parallele Programmierung, McGraw-Hill, 1992
[19] Rajasekaran, Sanguthevar [Hrsg.]: Handbook of parallel computing, Chapman & Hall/CRC, 2008
[20] Ullenboom, Christian: Java ist auch eine Insel, Galileo Press, 2005
[21] Ungerer, Theo: Parallelrechner und parallele Programmierung, Spektrum, Akad. Verl. 1997
[22] Wilkinson, Barry: Parallel programming, Pearson/Prentice Hall, 2005
[23] Zhou, Xingming [Hrsg.]: Advanced parallel processing technologies, Springer, 2003
[24] Zöbel, Dieter: Konzepte der parallelen Programmierung, Teubner-Verlag, 1988
Literaturverzeichnis
109
[25] Angelika Langer: Multithread Grundlagen
(http://www.angelikalanger.com/Articles/EffectiveJava/12.MT-Basics/12.MT-Basics.html), 31.10.2010
[26] Angelika Langer: ThreadPools
(http://www.angelikalanger.com/Articles/EffectiveJava/20.ThreadPools/20.ThreadPools.html), 31.10.2010
[27] Forschungszentrum Karlsruhe: Leistungskriterien für parallele Programme
(http://hikwww2.fzk.de/hik/orga/hlr/AIX/testen/), 31.10.2010
[28] Max-Planck-Institut für Metallforschung: Physik auf Parallelrechnern
(http://www.mf.mpg.de/mpg/websiteMetallforschung/pdf/05_Serviceeinrichtungen/
Datenverarbeitung/vorlesungen/PaPR1.pdf), 31.10.2010
[29] Stephan Schmidt: Skalierbarkeit (http://www.deutsche-startups.de/?p=14278), 31.10.2010
[30] TU Wien: Mehrprozessorsysteme (http://gd.tuwien.ac.at/study/hrh-glossar/12-1_1.htm), 31.10.2010
[31] TU Dresden: Parallelisierung (http://tudresden.de/die_tu_dresden/zentrale_einrichtungen/zih/dienste/rechner_und_
arbeitsplatzsysteme/hochleistungsrechner/parallel#newNavigationList), 31.10.2010
[32] Uni Karlsruhe: Parallelrechner (http://www.rz.uni-karlsruhe.de/rz/hw/sp/onlinekurs/PARALLELRECHNER/), 31.10.2010
[33] TU Berlin: Parallele Programmiermodelle (http://kbs.cs.tu-berlin.de/ivs/Lehre/SS04/VS/), 31.10.2010
[34] Christian Ullenboom: Java (http://openbook.galileodesign.de/javainsel5/), 31.10.2010
[35] Doug Lea: Doug Lea's Home Page (http://gee.cs.oswego.edu/dl/), 31.10.2010
[36] Doug Lea: Fork/Join-Framework (http://gee.cs.oswego.edu/dl/papers/fj.pdf), 31.10.2010
[37] Doug Lea: Concurrency (http://gee.cs.oswego.edu/dl/classes/EDU/oswego/cs/dl/util/concurrent),
31.10.2010
[38] Brian Goetz: Learn how to exploit fine-grained parallelism using the fork-join framework
(http://www.ibm.com/developerworks/java/library/j-jtp11137.html#listing3), 31.10.2010
[39] Stephan Schmidt: Concurrency (http://codemonkeyism.com/concurrency-rant-different-types-ofconcurrency-and-why-lots-of-people-already-use-erlang-concurrency/), 31.10.2010
[40] Doug Lea: Parallel Decomposition (http://zone.ni.com/devzone/cda/tut/p/id/6616) , 31.10.2010

Similar documents