Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                

Paralleles Programmieren mit Java

Paralleles Programmieren mit Java Heinz Kredel Akitoshi Yoshiday Zusammenfassung Parallele Programmierung wird oft mit High Performance Computing assoziiert aber mit dem Programmiersystem Java stehen alle Hilfsmittel fur die Nutzung dieser Techniken in beliebigen Anwendungen zur Verfugung. Wir besprechen die parallele Programmierung von Rechnern mit gemeinsamem Speicher, im sogenannten Thread Modell, und von Rechnern mit verteiltem Speicher, im Netzwerk- oder KommunikationsModell. Neben den wesentlichen Sprachkonstrukten und Klassen gehen wir auf einige Probleme und ihre Losung mit den Mitteln von Java ein. Den Abschlu bilden die neuesten Entwicklungen, wie zum Beispiel RMI, CORBA und Java Grande. 1 Einleitung Auch wenn paralleles Programmieren schon einige Zeit erforscht ist und geeignete Werkzeuge zur Implementierung vorhanden sind, werden diese doch nur von wenigen Programmieren konsequent genutzt. In diesem Artikel wollen wir zeigen, wie mit der Programmiersprache Java [1] und der entsprechenden Bibliotheksumgebung parallele Programme geschrieben werden konnen. Dabei konnen wir in der Kurze nur die wichtigsten Sprachkonstrukte und Klassen vorstellen. Es wird aber ausreichen, um Sie von der Leistungsfahigkeit von Java fur dieses Gebiet zu uberzeugen. Auch wenn die Java Programme hinterher nicht die Performance von numerischen FORTRAN oder C++ Programmen erreichen, so erleichtert Java und seine Umgebung doch sehr die Entwicklung von vielen parallelen Anwendungen. Fur Java spricht auch die allgemeine Verfugbarkeit, und die gute Integration aller fur die parallele Programmierung wichtigen Hilfsmittel. Somit konnen auch Auszubildende, ohne Zugang zu einem Parallelrechner zu benotigen, die Grundlagen der parallelen Programmierung zu lernen. Im Rest dieses Abschnitts fuhren wir kurz in die Problematik des parallelen Programmierens ein. Dann besprechen wir im nachsten Abschnitt 2 zunachst  Rechenzentrum Universit at Mannheim, e-Mail: kredel@rz.uni-mannheim.de y SAP AG Walldorf, e-Mail: Akitoshi.Yoshida@sap-ag.de 1 Threads und ihre Programmierung und anschlieend in Abschnitt 3 die Programmierung der Kommunikation. In Abschnitt 4 gehen wir auf neuere Entwicklungen ein. Eine ausfuhrlichere Besprechung aller notwendigen Konzepte und ihrer Implementierung mit Java nden Sie in unserem Buch [4]. Um den Umfang fur diesen Artikel nicht zuuberschreiten, setzen wir im folgenden elementare Kenntnisse in Java voraus. 1.1 Problemstellung Die Hardware-Entwicklung der letzten Jahre und Jahrzehnte hat zu einer weiten Verbreitung von Multitasking-Betriebssystemen, Multiprozessor Rechnern sowie zu Netzwerken von Workstations und PCs gefuhrt. Eine Konsequenz dieser Entwicklung war es, da nun verschiedene (Berechnungs-) Aufgaben gleichzeitig und nebeneinander bearbeitet werden konnen. Die Programmierung dieser Systeme wird als Parallele oder Nebenlau ge Programmierung bezeichnet; im Englischen wird etwas tre ender von `concurrent programming' gesprochen. Die Software mit der die Systeme programmiert werden, hat folgende Hilfsmittel, um die gleichzeitige Bearbeitung auszudrucken:      Erzeugen und Anstoen von Aufgaben Synchronisierung des Zugri s auf Ressourcen Koordinierung der Ressourcen De nition von Verbindungen (Kanalen) zur Kommunikation Informationsaustausch zwischen nebenlau gen Programmen Aus der Sicht eines Programmierers besteht die Aufgabe wahrend der Entwicklung von Programmen in der geeigneten wechselseitigen Abstimmung des Algorithmus und der verwendeten Datenstrukturen. In der Parallelen Programmierung gilt es { unabhangig von der zugrundeliegenden Hardware { die verschiedenen Moglichkeiten der Datenhaltung zu berucksichtigen: Daten im gemeinsamen (Haupt-)Speicher oder Daten verteilt auf lokale Speicher in vernetzten Rechnern. Im ersten Fall mu der gemeinsame Zugri mehrerer Programme auf diese Daten synchronisiert werden; im zweiten Fall mussen die Daten zwischen den Rechnern transportiert werden. Die Optimierung der Programme bedeutet im ersten Fall eine Verringerung der Zugri skon ikte, im zweiten Fall eine Verringerung des Datentransports. Zur Implementierung paralleler Programme stehen im Wesentlichen zwei Techniken bereit: Prozesse oder Threads. Prozesse sind eigenstandige Programme, die vom jeweiligen Betriebssystem unabhangig voneinander zur Ausfuhrung gebracht werden. Unter DOS und Windows sind das also die EXE-Dateien und unter Unix die normalen Binaries (a.out-Dateien). Prozesse konnen uber verschiedene Hilfsmittel mit anderen Prozessen in Kontakt treten: TCP/IP Sockets, 2 Message Passing Bibliotheken (wie PVM [2] oder MPI [3]), Pipelines (uber die Standard-Eingabe und -Ausgabe) oder uber gemeinsame Speicherbereiche, die vom Betriebssystem angefordert werden. Threads, auch Ausfuhrungsfaden genannt, sind keine eigenstandigen Programme, sondern vielmehr Teile von Prozessen. In jedem Thread wird ein Unterprogramm ausgefuhrt, das uneingeschrankten Zugri auf alle globalen Daten des umgebenden Prozesses hat. Zu den globalen Daten gehoren globale Variablen, Datei-Handles und auch Netzverbindungen. Nur die lokalen Daten der Unterprogramme sind in jedem Thread verschieden. 2 Thread Programmierung Die Thread Funktionalitat kann in Programmiersprachen durch eigene Sprachkonstrukte oder durch externe Bibliotheken realisiert sein. In Java stehen Threads als Basisklassen zusammen mit Spracherweiterungen zur Verfugung. In Ada gehoren Threads, hier \Tasks" genannt, zum Sprachumfang. Als minimale Losung werden in C, C++, Modula-2, FORTRAN und anderen Programmiersprachen Threads durch sprachunabhangige Programmbibliotheken angeboten. Eine standardisierte Bibliothekslosung stellt die POSIX Threads Bibliothek \Pthread" dar. Sie ist in OSF DCE (Open Software Foundation, Distributed Computing Environment) enthalten, wird aber auch auf verschiedenen anderen Systemen angeboten. POSIX bedeutet Portable Operating System IX, ein Standardisierungskommitee von IEEE. Auch die Java Threads werden - je nach Plattform - mit Hilfe von POSIX Threads implementiert. Thread Bibliotheken mit ahnlicher Funktionalitat wie POSIX Threads sind z.B. implementiert auf OS/2 und Windows NT. Wichtig fur eine eziente Implementierung von Threads ist, da Threads schon vom Betriebssystem bereitgestellt werden und nicht aufgesetzt sind. In jedem Proze (siehe Abbildung 1) wird zunachst vom Betriebssystem ein Hauptthread erzeugt und gestartet. Dieser kann nun weitere Threads erzeugen, die wiederum weitere Threads generieren konnen. Alle Threads konnen auf die gemeinsamen Ressourcen (Speicher, Dateien, Netzverbindungen) zugreifen und diese auch modi zieren. Falls ein Thread alle seine ihm zugedachten Aufgaben getan hat, kann er terminieren. Die Zustande, in denen sich Threads (wie auch Prozesse) be nden konnen, sind: running, blocked, ready oder terminated. Die Java-Implementierung von Threads, die in dem Java-Package java.lang enthalten ist, besteht aus folgenden Teilen: 1. der Klasse Thread und dem Interface Runnable, 2. dem Java-Sprachkonstrukt synchronized und 3. den Methoden wait(), notify() der Basisklasse Object. 3 Abbildung 1: Threads innerhalb von Prozessen z Hauptthread Erzeugung z ? Erzeugung z z z z z z z Threads ? ? ? ? ? ? Join ? Threads zz z z ?? ? ? ? ? Speicher Prozess Dateien ? Erzeugung zz z z ? ? ? ?Join ?Join Netzverbindungen Die Bedeutung und die Anwendung dieser Teile besprechen wir in den nachsten drei Abschnitten. 2.1 Thread Erzeugung In Java mu zur Erzeugung von Threads in ublicher Weise eine geeignete Klasse instanziiert werden, und dann wird im richtigen Moment eine Methode dieser Instanz aufgerufen. Fur die Klasse gibt es zwei Moglichkeiten   Bildung einer Subklasse der Thread-Klasse Bildung einer Klasse, die das Runnable Interface implementiert. Die De nition als Subklasse von Thread hat den Vorteil, da bequem alle Methoden der Thread-Klasse geerbt und somit verwendet werden konnen. Der 4 wesentliche Nachteil dieses Ansatzes wird dadurch verursacht, da Java keine Mehrfachvererbung erlaubt. Wenn zum Beispiel eine Klasse von der Klasse Applet abgeleitet wird, kann sie nicht gleichzeitig von Thread abgeleitet werden. In der Regel wird daher ein Thread durch eine Klasse de niert, die das Runnable-Interface implementiert. Das Runnable-Interface verlangt nur die Implementierung der Methode public void run() und hat keine weiteren Anforderungen. Wir bevorzugen meist die zweite Methode. Zur Vereinfachung der Ausdrucksweise werden wir oft von Runnable-Klassen sprechen, wenn wir Klassen meinen, die das Runnable-Interface implementieren. Der Thread-Konstruktor und die Thread-Methoden start() und join() haben die folgenden Spezi kationen. public public public public public Thread(Runnable target) Thread(Runnable target, String name) Thread(ThreadGroup group, Runnable target, String name) synchronized void start() final void join() throws InterruptedException Mit einer ThreadGroup lassen sich Gruppen von Threads de nieren. Auf die gesamte Gruppe lassen sich dann Operationen zur Steuerung der MitgliederThreads anwenden. Falls der Thread keiner besonderen ThreadGroup angehoren mu und er auch keinen Namen haben braucht, reicht die erste Variante Thread(Runnable target). Mit der start()-Methode von Thread wird dann die run()-Methode der Klasse myRunnable gestartet. Mit der join()-Methode von Thread kann auf die normale Beendigung des Threads gewartet werden. Thread kennt noch die stop()-Methode, mit der ein laufender Thread abgebrochen werden kann. Normalerweise wird man stop() selten benutzen und lieber ein Verfahren implementieren, das zu einer normalen Terminierung der Threads fuhrt. stop() soll ab Java 1.2 nicht mehr verwendet werden. Um einen ersten Eindruck eines parallelen Java-Programms zu bekommen, betrachten wir folgende Programmteile. class Action implements Runnable { int var; public Action(int v) { var = v; } public void run() { doSomeWork(var); } } Der erste Teil de niert eine Klasse, die Runnable implementiert. Der Konstruktor nimmt einen Parameter entgegen, der dann in der Funktion doSomeWork() in der run()-Methode verwendet wird. Die run()-Methode wird spater von der jeweiligen start()-Methode gestartet. Thread t1 = new Thread(new Action(1)); Thread t2 = new Thread(new Action(2)); 5 Thread t3 = new Thread(new Action(3)); try { t1.start(); t2.start(); t3.start(); t1.join(); t2.join(); t3.join(); } catch (InterruptedException e) { ... } Der zweite Teil zeigt die Erzeugung von drei Threads t1, t2 und t3. Die Threads werden jeweils mit einer neuen Instanz der Klasse Action erzeugt. Dann werden die Threads der Reihe nach mit ti.start() gestartet. Anschlieend wird mit drei ti.join() auf die Terminierung der Threads gewartet. 2.2 Synchronisation von kritischen Bereichen Da innerhalb der verschiedenen run()-Methoden globale Variablen vorkommen konnen, kann es passieren, da gleichzeitig auf ein und dieselbe Variable zugegriffen wird. An diesen Stellen ist einige Vorsicht geboten, da sonst die Werte dieser Variablen am Programmende falsche oder zufallige Werte enthalten konnen. Um diese unerwunschten U berlappungen zu vermeiden, ist es erforderlich, da sich Programmteile gegenseitig ausschlieen (mutual exclusion) oder gegebenenfalls die Abarbeitungsschritte aufeinander abstimmen (condition synchronization). Da wir nicht verhindern konnen, da Schreib- oder Lese-Operationen auf den globalen Speicher in nebenlau gen Prozessen statt nden und sichtbar werden, benotigen wir einen Trick. Dieser besteht darin, den Thread in der Ausfuhrung anzuhalten, der die Zustandsanderungen nicht sehen soll. Dies setzt voraus, da wir bei Bedarf nicht nur lokal ein Haltekonstrukt einfugen, sondern wir mussen alle gefahrdeten \kritischen" Bereiche in allen Threads aus ndig machen und dort ebenfalls ein passendes Haltekonstrukt einfugen. Dies ist keine so groe Einschrankung wie es zunachst aussehen mag, denn wir konnen die Variablen, die in kritischen Statements vorkommen, oft in einer Klasse isolieren und ihre Modi zierung durch geeignete Methoden kontrollieren. Das Anhalten der Threads erreichen wir durch das synchronized-Sprachkonstrukt. Das Java-Sprachkonstrukt synchronized hat die folgenden Varianten. synchronized (object) { ... } synchronized (static object) { ... } synchronized type methodName(...) { ... } static synchronized type methodName(...) { ... } Die erste Variante benutzt eine Objektinstanz object einer beliebigen, von der Object-Klasse abgeleiteten Klasse als Haltepunkt. Das heit: wird in verschiedenen Threads ein synchronized (object) auf ein bestimmtes Objekt object ausgefuhrt, so stellt das Java-Laufzeitsystem sicher, da immer nur maximal ein Thread die Statements {...} ausfuhren kann. Man spricht dann auch davon, 6 da man einen \lock" das Objekt setzt (es also \verschliet"). Ein \lock" kann immer nur einmal zu einem gegebenen Zeitpunkt aktiv sein. Man spricht dann auch von gegenseitigem Ausschlu (\mutual exclusion"). Die \lock"-Objekte werden dann auch als \mutex" bezeichnet. Falls das Objekt als static deklariert ist, ndet ein \lock" Systemweit nur einmal statt, da das Objekt nur einmal vorhanden ist, sonst bezieht sich der \lock" nur auf eine Instanz eines Objekts. Das heit, mehrere Instanzen eines Objekts haben dann verschiedene \locks", die untereinander nicht synchronisiert sind. Die Semantik von synchronized type methodName(...) { S1; ...; Sn; } entspricht der von type methodName(...) { synchronized(this) { S1; ...; Sn; } } Dies zeigt auch, da Methoden einer Klasse, die nicht als synchronized deklariert sind, frei auf die Instanzendaten zugreifen konnen es ndet keine Synchronisation statt. Damit stellt Java auch keine echten Monitore, wie sie von Hoare entwickelt wurden, zur Verfugung. Nur wenn alle Nicht-private-Methoden synchronized sind und es nur private-Variablen gibt, erh alt man etwas ahnliches wie einen Monitor. 2.3 Warten auf Bedingungen Nachdem wir die kritischen Bereiche im Gri haben, stellen wir uns dem letzten Problem. Wenn wir zum Beispiel eine Summe in einer globalen Variablen bilden wollen, mussen wir den Zugri darauf mit synchronized() kontrollieren, aber bevor wir uberhaupt anfangen zu summieren, mu sicher sein, da die Variable initialisiert ist. Bei dem Summen-Beispiel konnen wir das machen, in dem wir die Threads erst nach der Initialisierung erzeugen. Bei komplexeren Datenstrukturen werden wir aber unter Umstanden die Initialisierung mit in die Threads einbeziehen. In dieser Situation benotigen wir eine Moglichkeit zu warten, bis dieser Schritt abgeschlossen ist. Zur Verfugung stehen uns die Object-Funktionen wait() und notify() mit den folgenden Spezi kationen: public final void wait() throws InterruptedException public final void wait(long timeout) throws InterruptedException public final void notify() public final void notifyAll() 7 Bei Aufruf von wait() wird der aufrufende Thread in den Wartezustand versetzt (\blocked"), und gleichzeitig wird der Lock auf diesen Abschnitt freigegeben. Der Thread wartet nun, bis ein anderer Thread die notify()-Methode dieses Objekts aufruft oder bis ein Timeout statt ndet. Dann wartet der Thread eventuell noch einmal, bis er den Lock auf den Abschnitt wieder erhalt, und wird dann wieder \ready" (\runnable"). notify() weckt genau einen Thread im wait-Zustand auf. Falls mehrere Threads warten, ist nicht vorhersehbar oder bestimmbar, welcher Thread aufgeweckt wird. Die Methode wait() darf nur innerhalb eines Abschnitts aufgerufen werden, der mit synchronized geschutzt ist. Die notify entsprechenden Funktionen in anderen Thread-Paketen (wie signal in Pthreads) garantieren nicht, da nur genau ein Thread aufgeweckt wird. Dort wird spezi ziert, da mindestens ein Thread aufgeweckt wird. notifyAll weckt alle an diesem Objekt wartenden Threads auf. Varianten von wait() mit timeout sind das `timed wait', bei dem eine Zeit angegeben wird, wie lange maximal auf das Eintreten einer Bedingung gewartet werden soll. Falls die Zeit verstrichen ist, terminiert die Methode, als ob ein notify() stattgefunden hatte. Das heit, auch der Lock wird dann wieder von diesem Thread gehalten. Es mu nun die Bedingung erneut getestet werden, um festzustellen, ob nur die Zeit abgelaufen ist oder die Bedingung tatsachlich erfullt ist. Wenn wir auf die Erfullung eines beliebigen Booleschen Ausdrucks warten wollen, mussen wir fur jede semantisch verschiedene Bedingung eine eigene Bedingungsvariable (d.h. ein eigenes Objekt) einfuhren. Fur jede Bedingung wird dann ihr Test, ob sie wahr ist, an alle (wichtigen) Stellen in das Programm verlegt, an denen die Bedingung wahr geworden sein konnte, und dort wird, falls ja, mit notify (oder notifyAll) der Eintritt der Bedingung signalisiert. Da von mehreren Threads gleichzeitig oder kurz hintereinander ein notify ausgelost werden kann, ist es fur den wartenden Thread wichtig, die Verzogerungsbedingung erneut zu testen, da ein weiterer Thread sie in der Zwischenzeit schon wieder ungultig gemacht haben konnte. Ein weiteres Problem entsteht dadurch, da z.B. notify() vor wait() ausgefuhrt werden konnte, was den einen Thread auf immer blockieren wurde (sogenannte \lost signals"). Eine Losung zu diesem Problem stellen Semaphore dar, die im Buch [4] in Abschnitt 3.4 besprochen werden. 2.4 Zusammenfassung Wir haben die wichtigsten Java Sprachkonstrukte zur Thread Programmierung vorgestellt. Anwendungen sind zum Beispiel Semaphore, Barrieren und das Bounded-Bu er-Problem, sowie die Verwendung von Threads in der AppletProgrammierung, auf die wir hier aber in der Kurze nicht eingehen konnen. Auch in den Enterprise Java Beans und in den neuen Java Swing Klassen werden Threads extensiv angewendet. 8 3 Programmierung der Kommunikation Mehrprozessorsysteme ohne gemeinsamen Hauptspeicher (wie z.B. WorkstationCluster) benotigen spezielle Leitungen zur Kommunikation. (Ethernet und TCP/IP bei Workstation-Clustern, Dateien (bzw. Pipes) bei Unix-Prozessen). Die Programmierung solcher Systeme erfordert daher die De nition von geeigneten Leitungen und U bertragungsprotokollen. Zur Kommunikation werden explizite Befehle zum Senden und Empfangen von Daten benotigt. Der Schutz von gemeinsamen Variablen ist nicht mehr erforderlich, da alle Programmteile als kritische Bereiche ausgefuhrt werden. Das Schema des Nachrichtenaustauschs ist in Abbildung 2 dargestellt. # "! # "! Abbildung 2: Schema des Nachrichtenaustauschs Verbindungskanal Empfanger Sender ? Netzkarte - 6 Netzwerk Netzkarte Die Java-Socket-Kommunikation basiert auf den durch das Internet bekannten TCP/IP Sockets. Diese Sockets werden mittlerweile von praktisch allen Betriebssystemen unterstutzt. Auch nahezu jede Treiber-Software von Netzwerk Hardware (Leitungen und Rechnerkarten) bietet Unterstutzung fur TCP/IP und damit fur Sockets. Insbesondere unterstutzen auch High Performance Netzwerke TCP/IP { wenn auch manchmal mit schlechterer Performance als speziell angepate Software. Sockets stellen aus Programmiersicht die Software-Schnittstelle zu einem Netzwerk dar. Sie entsprechen etwa den Filehandles, die fur den Zugri auf Plattendateien angelegt und verwaltet werden mussen. Java-Netzwerk-Support be ndet sich im Package java.net. Wir beschranken uns in diesem Artikel auf die Erlauterung des einfachsten Falls einer einzigen Punkt-zu-Punkt Verbindung. Zum Beispiel konnen wir damit 9 nicht direkt 1-zu-n- oder m-zu-n-Verbindungsnetzwerke realisieren. 3.1 Aufbau von Verbindungskanalen Zur Implementierung dieser Klassen verwenden wir von Java die Klassen und Socket. Um eine Kontrolle uber einen korrekten Aufbau eines Kanals zu bekommen, wird die Socket-Verbindung unsymmetrisch aufgebaut. Ein Kanalende (der Server Socket) baut seine Datenstrukturen auf und wartet dann auf einen Verbindungswunsch vom anderen Ende. Das andere Kanalende (der Client Socket) baut ebenfalls zuerst seine Datenstrukturen auf und versucht dann, eine Verbindung zur anderen Seite aufzubauen. Gelingt dies auf beiden Seiten, besteht ein zuverlassiger Verbindungskanal. Es wird also nicht erst bei einem folgenden Senden und Empfangen festgestellt, da eine Verbindung gar nicht zustande kam. Die Spezi kation des ServerSocket-Konstruktors und der benotigten Methode ist wie folgt. ServerSocket public ServerSocket(int port) throws IOException public Socket accept() throws IOException Der Konstruktor ServerSocket erzeugt einen neuen Server Socket an dem angegebenen port. Eine Portnummer 0 erzeugt einen Socket an einem beliebigen freien Port. Die Funktion accept() wartet auf einen Verbindungswunsch auf dem entsprechenden Port und gibt dann einen Socket fur die Verbindung zuruck. Die Funktion blockiert solange, bis eine Verbindung zustande kommt. Es folgen die Spezi kationen des Socket-Konstruktors und der benotigten Methoden. public Socket(String host, int port) throws UnknownHostException, IOException public InputStream getInputStream() throws IOException public OutputStream getOutputStream() throws IOException Der Konstruktor Socket erzeugt einen neuen Client Socket zu dem angegebenen host und port. Der Socket stellt einen Eingabe- und einen AusgabeStrom zur Verfugung, auf die mit den Methoden getInputStream() und getOutputStream() zugegri en werden kann. Ein Strom ist eine unformatierte und unstrukturierte Folge von Daten. Die Strome InputStream und OutputStream bestehen aus Folgen von Bytes. Zu den Eingabe- und Ausgabe-Stromen kann nun ein zu den Anforderungen passender Datenstrom zu geordnet werden. Fur Folgen von reinen Unicode Zeichen konnten wir die Strome Reader und Writer einsetzen. Wir verwenden in diesem Abschnitt ObjectStreams, da damit sehr exibel beliebige (serialisierbare, serializable) Objekte uber den Kanal ausgetauscht werden 10 konnen. Ein Objekt-Strom kann einfache und zusammengesetzte Java-Objekte in einen Daten-Strom umwandeln und typsicher versenden oder empfangen. Daten-Strome konnen sowohl Datei-Strome als auch Netzwerk-Socket-Strome sein. Es werden nur Objekte, die das java.io.Serializable Interface implementieren in den Objekt-Stromen akzeptiert. Objekt-Serialisierung ist eins der wesentlichen neuen Features von Java 1.1. Falls eine Klasse Serializable implementiert, wird die Objekt-Serialisierung von Java automatisch durchgefuhrt. U berschreibt man die automatische Serialisierung, mu man die Klasse selbst kodieren und senden. Bei manchen Objekten wie Filehandles macht die Serialisierung naturlich keinen Sinn, denn ein Filehandle zeigt vielleicht auf eine Datei, die auf einem anderen Rechner nicht existiert. Die Spezi kation der benotigten Konstruktoren und Methoden ist wie folgt. public ObjectOutputStream(OutputStream out) throws IOException public void flush() throws IOException public ObjectInputStream(InputStream in) throws IOException, StreamCorruptedException Der Konstruktor ObjectOutputStream erzeugt einen neuen Objekt-Ausgabestrom, zu einem gegebenen OutputStream. Zuerst wird ein Strom-Header geschrieben, der eine eindeutige Identi kation der Objekt-Strom-Klasse enthalt. Mit flush() wird sichergestellt, da dieser Header unmittelbar verschickt wird, damit die Gegenseite die Daten sofort einlesen kann. Der Konstruktor ObjectInputStream erzeugt einen neuen Objekt-Eingabestrom, zu einem gegebenen InputStream. Zuerst wird ein Strom-Header gelesen. Passt die Identi kation im Header nicht zu der eigenen, weil beispielsweise die Daten von einem anderen Rechner mit einer inkompatiblen JDKVersion uber das Netz kommen, so wird ein Fehler ausgelost (hier ist das StreamCorruptedException). Der Konstruktor blockiert, bis ein Objekt-Ausgabe-Strom die entsprechenden Daten gesendet hat. 3.2 Senden und Empfangen Zur eigentlichen Datenubertragung benotigen wir auf einer Seite eine SendOperation (send) und auf der anderen Seite eine Empfangs-Operation (recieve). W ahrend der Aufruf der Empfangs-Operation immer blockierend ist, kann die Send-Operation sowohl synchron als auch asynchron arbeiten: = das Programm wird nach dem Einstellen der Daten in einen Sendepu er ohne weitere Verzogerung fortgesetzt, d.h., send terminiert auch, wenn die Daten erst sehr viel spater bei einem Empfanger ankommen. asynchronous send 11 = die weitere Programmausfuhrung wird nach dem send so lange blockiert, bis ein entsprechendes receive ausgefuhrt wurde, d.h. bis die Daten von einem Empfanger wirklich abgenommen worden sind. synchronous send Das asynchrone send erfordert einen potentiell unbeschrankt groen Pu er, wahrend das synchrone send nur einen Pu er fester Groe erfordert. Die Verwendung des synchronen send ist schwieriger, da es sehr genau auf die zeitlich richtige Reihenfolge aller send- und receive-Operationen ankommt. Java unterstutzt mit den Socket-Klassen nur das asynchrone send. Zur Implementierung der Send-Operation mit Java stehen uns die Funktion writeObject() aus der Klasse ObjectOutputStream mit der folgenden Spezikation zur verfugung. public final void writeObject(Object obj) throws IOException Die Methode writeObject() schreibt ein Objekt auf den entsprechenden Ausgabe-Strom. Der gesamte Graph des Objekts wird zerlegt (serialisiert), verpackt und zusammen mit dem Klassennamen und der Klassensignatur geschrieben. Fur die Empfangs-Operation steht uns die Java-Funktion readObject(), aus der Klasse ObjectInputStream zur verfugung, die die folgenden Spezi kation hat. public final Object readObject() throws OptionalDataException, ClassNotFoundException, IOException Die Methode readObject() liest ein Objekt von dem entsprechenden EingabeStrom. Der gesamte Graph des Objekts wird gelesen, entpackt und rekonstruiert. 3.3 Zusammenfassung Wir haben in diesem Abschnitt die wichtigsten Java-Hilfsmittel zur Programmierung der Kommunikation zwischen Prozessen kennengelernt. Fur parallele Anwendungen, die uber das Client-Server-Schema hinausgehen, werden allerdings komplexere Implementierungen benotigt, die wir in [4] im Kapitel 5 diskutieren. 4 Ausblick Im letzten Abschnitt geben wir einen U berblick uber die Themen RMI, CORBA und Java Grande, die im Zusammenhang mit paralleler Programmierung wichtig sind. 12 4.1 RMI Die Java Remote Method Invocation Technik (RMI), erlaubt die Ausfuhrung von Methoden auf einem entfernten Rechner. Die Eingabeparameter werden uber eine Netzverbindung zu einem entfernten Rechner geschickt, die entsprechende Methode wird dort ausgefuhrt und anschlieend wird der Ruckgabewert uber die Netzverbindung zuruck geschickt. Java RMI ist das Analogon zu Remote Procedure Call (RPC), das in der Kommunikationstechnik schon viele Jahre eingesetzt wird. Abbildung 3: Remote Method Invocation  Stub   Repository 6     - lookup (re)bind call result Client A ?    Skeleton Method B Server Die RMI Architektur wird in Abbildung 3 gezeigt. Computer 'A' bezeichnet den Rechner, von dem aus eine Methode auf dem entfernten Rechner 'B' aufgerufen werden soll. 'Client' bezeichnet den Benutzerprozess in Rechner A. 'Server' bezeichnet den Proze, der die aufrufbare Methode bereitstellt. Damit die verwendbaren Methoden nicht im Programmcode auf der Server Seite fest angegeben werden mussen, wird ein Verzeichnis, genannt 'Repository', auf Rechner B gefuhrt. Dieses Verzeichnis mu vor beginn aller RMI Aktivitaten gestartet sein. Server, die Methoden bereitstellen wollen verwenden rmi.Naming.bind() oder rmi.Naming.rebind(), um eine Klasseninstanz mit einem Namen bei dem Repository zu registrieren. Clients, die Methoden verwenden wollen sehen mit rmi.Naming.lookup() bei dem Repository nach ob ein gewunschtes Objekt vorhanden ist und verknupfen es mit einem lokalen Objekt. 13 Beim Aufruf einer entfernten Methode im Client wird zunachst eine sogenannte 'Stub' Methode aktiviert. Diese sendet die Eingabeparameter (mit einer Bezeichnung der entfernten Methode) an eine 'Skeleton' genannte Methode auf dem Server. Die Skeleton Methode ruft dann auf dem Rechner B die gewunschte Methode auf und schickt den Returnwert nach der Terminierung der Methode an die Stub Methode zuruck. Diese ubergibt den Wert dann an die aufrufende Prozedur. Der Einsatz von Stub und Skeleton erfolgt transparent, d.h. ohne spezielle Aufrufe seitens der Programmierers. 4.2 CORBA Nachdem es zunachst aussah, als ob Java RMI als direktes Konkurrenzprodukt zu CORBA entwickelt werden wurde, hat sich mittlerweile die Einsicht durchgesetzt, da eine Zusammenarbeit fur beide Seiten viele Vorteile bringt. Mit dem JDK 1.2 wird Java volle CORBA Unterstutzung bieten. Zusatzlich entwickeln Sun und IBM die Unterstutzung von RMI uber das IIOP (Internet Inter-ORB Protokoll). Damit konnte auch uber RMI auf CORBA Objekte zugegri en werden. Der Name CORBA ist die Abkurzung fur Common Object Request Broker Architecture. CORBA ist eine Spezi kation, das heit eine De nition von Schnittstellen, die das Zusammenwirken von von Objekten in verteilten Umgebungen beschreibt. Die wesentlichen Punkte dieser Spezi kation sind:    CORBA Objekte sind durch die ORB (Object Request Broker) Kommunikations-Infrastruktur transparent von Clients und Servern zugreifbar und benutzbar. CORBA ist unabhangig von einer bestimmten Programmiersprache. Es gibt unter anderem CORBA Anbindungen fur Ada, Cobol, C++, Smalltalk und Java. CORBA ermoglicht sogenannte `Multi-Tier' Systeme. Das heit mehrschichtige verteilte Software-Architekturen. Statt einfacher Client-Server System lassen sich mehrstu ge Systeme aus Clients, mehreren verschiedenen Anwendungsservern und Hintergrundprozessen wie Datenbanken realisieren. Zur Kommunikation zwischen ORBs und Objekten mit Internet Mitteln gibt es das Internet Inter-ORB Protokoll (IIOP). Das Prinzip der CORBA Architektur ist in Abbildung 4 zusammen gefat. Der ORB de niert die Infrastruktur fur das Zusammenwirken von Anwendern (Clients) und den durch verteilte Objekte bereitgestellten Diensten (Objekt Implementierungen). Vollstandige Informationen zu CORBA nden Sie im Internet auf dem WebServer der OMG: http://www.omg.org/. Eine deutschsprachige Einfuhrung in CORBA gibt auch das Buch von Sayegh [6]. Eine freiverfugbare, vollstandige Implementierung des CORBA 2.0 Standards ist MICO [5]. 14 Abbildung 4: Object Request Broker Objekt Implementierung Client & % 6 Objekt Benutzung ORB, Object Request Broker 4.3 Java Grande Obwohl es unbestreitbar ist, da Java zur Zeit im Vergleich zu FORTRAN oder C++ um mehr als den Faktor 10, also deutlich langsamer ist, werden dessen ungeachtet mittlerweile viele Java Anwendungen entwickelt, deren Design Ziele zunachst auf umfangreiche Funktionalitat ausgerichtet waren. Diese Anwendungen verlangen aber an bestimmten Stellen nummerische Berechnungen, zum Beispiel zu graphischen Darstellung der Ergebnisse. Zur Losung dieses Problems, d.h. zur Verbesserung der nummerischen Leistungen von Java haben sich Anfang 1998 verschiedene Entwickler zum \Java Grande Forum" zusammen gefunden. Unter andrem sind James Gosling von Sun und Marc Snir von IBM im Forum vertreten. Java Grande soll Java fur folgende Bereiche verbessern.       High Performance Network Computing technisch-wissenschaftliches Rechnen verteilte Modellierung und Simulation paralleles und verteiltes Rechnen Anwendungen mit sehr groen Datenmengen rechenintensive kommerzielle Anwendungen Die Ziele des Java Grande Forums sind:  Starkung des Potentials von Java als Entwicklungsumgebung fur \Grande applications". D.h. Java als bessere Entwicklungsumgebung als FORTRAN oder C++ fur groe umfangreiche Anwendungen. 15   Das Forum soll Konsens herstellen und Empfehlungen ausarbeiten fur Weiterentwicklungen von Java selbst, oder fur die Entwicklung von Standards (Frameworks) fur \Grande" Bibliotheken oder Dienste. Die Java Sprachanderungen und Frameworks sollen die beste je verfugbare Programmierumgebung fur groe umfangreiche (nummerische) Anwendungen werden. Unter dem Java Grande Forum haben sich zur Zeit zwei Arbeitsgruppen gebildet: eine mit Schwerpunkt Numerik und eine mit Schwerpunkt Parallel- und Verteiltes-Computing. Die wichtigsten Themen, die in den Arbeitsgruppen derzeit behandelt werden sind: volle Unterstutzung der IEEE 754 Floating Point Spezi kation durch Java, Arrays, Interfaces zu MPI, PVM, sowie zu Lapack und BLAS, Tools, Verbesserung des Laufzeitsystem und der verteilten Garbage Collection, bessere Berucksichtigung der Speicherhierarchie (Register, Caches, Hauptspeicher). Die zweite Arbeitsgruppe befat sich vorrangig mit der Verbesserung von RMI, Support fur Multicast, Unterstutzung der Programmiermodelle SPMD und MIMD; Verbesserung der Virtuellen Java Maschine, schnellere Synchronisierung, bessere Skalierbarkeit der Threads. Naheres zu Java Grande nden Sie im Internet unter dem folgenden URL1 http://www.npac.syr.edu/javagrande/. Zusammenfassung Wir haben in diesem Artikel kurz umrissen, da zum Lernen von paralleler Programmierung Java gut geeignet ist. Wo hohere Performance notwendig ist konnen Programmteile in C++ implementiert werden, auf die dann mittels Java Native Interface (JNI) zugegri en werden kann. In der Java Grande Initiative wird an der Verbesserung von Java fur Numerische und Parallele Anwendungen gearbeitet. Literatur [1] K. Arnold and J. Gosling. The Java Programming Language. AddisonWesley, 1996. [2] A. Geist, A. Beguelin, J. Dongarra, W. Jiang, R. Mancheck, and V. Sunderam. PVM 3.3 Usersguide. Oak Ridge, USA, 1995. [3] W. Gropp, E. Lusk, and A. Skjellum. Using MPI: Portable parallel programming with the Message Passing Interface. MIT Press, Cambridge, Mass., 1995. 1 Stand: 16. September 1998 16 [4] H. Kredel and A. Yoshida. Thread- und Netzwerk-Programmierung mit Java. Ein Praktikum fur die Parallele Programmierung. dpunkt.verlag, 1998. [5] A. Puder and K. Romer. MICO Is CORBA: A CORBA 2.0 compliant implementation. dpunkt.verlag, 1998. [6] A. Sayegh. CORBA Standard, Spezi kation, Entwicklung. O'Reilly, 1997. 17