MultiCore-Programmierung in Java

MultiCore-Programmierung in Java Bachelor-Arbeit (SS 2010) Betreuer: Dipl.-Ing. Dr. Hans Moritsch Verfasser: Gerold Egger Matr.-Nr. 8917007 Universi...
Author: Dirk Hausler
12 downloads 2 Views 875KB Size
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­  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  {    public ReturnType call() {     /* Belegung des ReturnType­Objekts */    }  }  ...  MyCallable task = new MyCallable( ReturnObj );  ExecutorService exe = Executors.newCachedThreadPool();  Future 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 { .....       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  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 [] "); 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 [] "); 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 futures = new ArrayList(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 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; }

[] ");

/** 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 { 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 [] "); 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 [] "); 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 = 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 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 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 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 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 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) { 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 " + " [ ]"); 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 threads = new ArrayList(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 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 " + " [ ]"); 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 futures = new ArrayList(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 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 " + " [ ]"); 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 " + " [ ]"); 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 "); 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 "); 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 futures = new ArrayList(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 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