André Vratislavsky
Seminar Verteilte Java-Systeme, SS 2000
Doorastha
Verteilungstransparente Programmierung mit Java
Das Doorastha-System von Markus Dahm / FU-Berlin
Zum Inhalt Einleitung
Die by-reference Semantik von Java
Vergleich mit Java RMI
Verteilungstransparenz
Möglichkeiten und Verwendung des Doorastha-Systems
Globalisierbare Objekte
Kopierbare Objekte
Anwendung der Tags: Objekte by-refvalue oder by-copy übergeben
Kopieren von Unterobjekten
Entfernte Erzeugung von Objekten
Klassenvariablen und Klassenmethoden
Client-Server Architekturen
Synchronisation von Threads
Variable Übergabearten
Migration: Wandernde Objekte und Threads
Ein Beispiel mit Doorastha
Hinter den Kulissen von Doorastha: Entwurf mit Java RMI
Globalisierbare Klassen
Interface (Schnittstelle)
Wrapperklasse (Hülle)
Proxyklasse (Stellvertreter)
Kopierbare Klassen
Dynamischer Fernzugriff
Innere Funktionsweise des Doorastha-Systems und Implementierung: Nur soviel Netzwerkaktivität wie nötig
Globalisierung von Objekten
Kopieren von Objekten
Migration von Objekten
Migration von Threads
Variable Übergabearten
Klassenvariablen und Klassenmethoden
Synchronisation von Threads
Anwendung des Doorastha-Systems
Das Doorastha Runtime System
Der Doorastha Compiler
Literatur
Doorastha ist eine Plattform für verteilte Java Applikationen.
Doorastha legt Wert auf große Verteilungstransparenz, d.h. für den Programmierer werden die Details der Verteilung so weit wie möglich und sinnvoll verborgen. Entfernte Objekte werden genau so wie lokale behandelt. So bleibt z.B. die gewohnte Semantik des ==Operators erhalten.
Wesentliche Aspekte verteilter Programme können dennoch feinkörnig gesteuert werden. So kann z.B. bestimmt werden, ob Objekten an entfernte Instanzen komplett oder teilweise als Referenzen oder Kopien übergeben werden sollen.
Doorastha erkennt automatisch, ob ein referenziertes Objekt entfernt oder auf demselben Rechner liegt, um unnötig nicht performante Aufrufe zu vermeiden.
Verteilte Programme sind in der Regel viel schwieriger zu entwerfen und zu programmieren als zentralisierte. Ebenso schwierig ist es, ein bestehendes zentralisiertes Programm an verteilte Verhältnisse anzupassen. Dabei können folgende Problemfelder unterschieden werden:
Die by-reference Semantik von Java
In Java erfolgt die Übergabe von Objekten an Methoden grundsätzlich durch Verweise (by-reference) auf diese Objekte. Diese Verweise selbst werden genauso wie primitive Datentypen als Wert (by-value) übergeben. Deshalb wird im folgenden von by-reference-value oder kurz by-refvalue gesprochen. Das "übergebene" Objekt bleibt an seiner Stelle im Speicher stehen. Es kann noch mehr Referenzen auf ein und dasselbe Objekt geben, weshalb sein Zustand nach einer Übergabe an eine Methode dynamisch durch andere Objekte verändern werden kann. Die Methode, an die ein Objekt "übergeben" wurde, kann dieses Objekt von außen sichtbar verändern, nicht aber die Referenz darauf, da Referenzen by-value, d.h. als Kopie übergeben werden. Es wäre wünschenswert, daß diese Semantik auch im verteilten Fall bestehen bliebe.
In Java’s RMI [SUN98] werden Argumente von entfernten Methodenaufrufen jedoch standardmäßig (sie müssen dazu aber das Interface Serializable implementieren) als Kopie, d.h. nicht als Verweis auf das Originalobjekt, an die entfernte Seite übergeben. Die Kopie beinhaltet das gesamte Objekt einschließlich seiner von seinen Variablen trannsitiv referenzierten Unterobjekte (deep copy). Wenn die Methode an dieser Objektkopie etwas ändert, bleibt das Original davon unberührt, was eine vollkommen andere Semantik gegenüber dem zentralisierten Fall zur Folge hat, denn das geklonte Objekt und das Original laufen in ihrem Zustand u.U. auseinander. Diese Semantik hat jedoch auch einen Vorteil, Zugriffe auf entfernte Objekte kosten nämlich sehr viel Zeit und sollten deshalb vermieden werden.
Soll in RMI [SUN98] ein Objekt in entfernt referenzierbarer Semantik an eine entfernte Methode übergeben werden, so muß dieses Objekt spezielle Eigenschaften haben: Die entsprechende Klasse muß von UnicastRemoteObject erben. Dasselbe gilt für die Klasse, aus der die entfernte Methode stammt. Diese Klasse muß ferner ein Interface implementieren, in dem die entfernten Methoden aufgeführt sind. Dieses Interface erbt von Remote, seine Methoden müssen public sein. Der direkte Aufruf entfernter Variablen statischer Methoden ist nicht möglich. Entfernte Klassen, welche Objektmethoden enthalten, die entfernt aufgerufen werden sollen, müssen durch entsprechende Vertreterobjekte repräsentiert werden (Skeletons auf Serverseite und Stubs auf Clientseite), die nach dem Compilieren speziell erzeugt werden müssen (in jdk1.2 mit dem Programm rmic). Klassenmethoden können nicht entfernt aufgerufen werden, auf Instanz- und Klassenvariablen kann ebenso wenig entfernt zugegriffen werden wie auf Systemklassen wie Hashtable, die nicht von UnicastRemoteObject erben.
Beispiel für ein zentralisiertes Programm:
Dasselbe in RMI:
Es ist in der Regel sicherlich sinnvoll bzw. einfacher, ein nebenläufiges Programm zunächst auf einem Rechner zu testen, bevor es auf ein verteiltes System gebracht wird. Das Doorastha-System ermöglicht, ein existierendes zentralisiertes (nebenläufiges) Programm an verteilte Verhältnisse anzupassen und dabei die by-refvalue Semantik des normalen Java beizubehalten. Damit wird es möglich, verteilungstransparent zu programmieren. Entfernte Zugriffe unterscheiden sich von lokalen syntaktisch und semantisch nicht.
Aus Effizienzgründen sollte jedoch die Frequenz entfernter Zugriffe klein gehalten werden. Daher ist es nicht sinnvoll, alle entfernten Aufrufe in der by-refvalue Semantik auszuführen. In Fällen, in denen die Semantik des Programms dadurch nicht beeinflußt wird, kann ein Objekt als Kopie übergeben werden. Dies wird sich auszahlen und nicht stören, wenn viele Lesezugriffe auf dieses Objekt stattfinden, aber keine Schreibzugriffe. Finden viele Methodenaufrufe an einem entfernten Objekt statt, oder wird ein entferntes Objekt nie in seinem Zustand verändert, so lohnt es sich, dieses Objekt auf die hiesige Seite zu kopieren oder sogar ganz zu verschieben. Die Verteilungstransparenz sollte also nicht so weit gehen, vor dem Programmierer zu verstecken, ob ein Objekt by-refvalue oder by-copy übergeben wird.
Das Doorastha-System ermöglicht, ein existierendes zentralisiertes (nebenläufiges) Programm an verteilte Verhältnisse anzupassen. Es kann vom Programmierer eines verteilten Programms oder von Werkzeugen wie Pangaea [Spi99], die ein zentralisiertes Programm automatisch verteilt umstrukturieren, verwendet werden. Wenn ein Programm unter dem Gesichtspunkt entworfen wird, Doorastha zu verwenden, kann bei der Entwicklung zunächst ein gewöhnlicher Java Compiler verwendet werden. Die für die Verteilung unter Doorastha nötigen Erweiterungen werden in das zentralisiert funktionstüchtige Programm in Form sogenannter Tags eingefügt. Doorastha erfordert keine zusätzlichen Sprachkonstrukte, sondern verwendet deklarative Markierungen. In der aktuellen Version werden dafür spezielle Kommentare verwendet. Daher ist der Quelltext auch mit einem Java Compiler übersetzbar und in vielen Fällen sogar sinnvoll zentralisiert ausführbar. Das Doorastha-System wertet die Tags aus und übersetzt die Klassen in Java Byte Code.
Das Doorastha Runtime System entscheidet dynamisch während der Laufzeit, ob ein als entfernt erreichbar gekennzeichnetes Objekt auch wirklich gerade auf einem entfernten Host liegt und es wirklich als entferntes Objekt behandelt werden, d.h. der Zugriff darauf über das Netz erfolgen muß. Lokale Verweise können automatisch in entfernte umgewandelt, entfernte Verweise auf lokale reduziert werden, wenn das referenzierte Objekt auf demselben Host liegt.
Der Verweis auf ein Objekt bezieht sich letztendlich immer auf das Original und nicht auf eine Kopie oder einen Vertreter. Deshalb kann z.B. der == Operator wie gewohnt angewandt werde und Objekte mit der equals() Methode verglichen werden. Die gewohnte Semantik von Verweisen über Objektvariablen wird beibehalten. Lokale und entfernte Methodenaufrufe unterscheiden sich im Prinzip nicht. Im Programmtext erscheint die Verteilung transparent, d.h. Verweise auf lokale und entfernte Objekte werden syntaktisch gleich behandelt. Trotzdem ist eine feinkörnige Steuerung der Verteilung möglich, denn bei Doorastha ist die entfernte Zugriffsmöglichkeit als Beziehung zwischen einzelnen Objekten definierbar und nicht wie bei RMI [SUN98] nur eine Eigenschaft von Klassen.
Doorastha ist in Java geschrieben, so daß das System auf jeder virtuellen Java Maschine läuft.
Java Funktionalitäten wie Threadsynchronisation, Garbagecollection und Portabilität bleiben erhalten.
Ein lokales Objekt kann fern erreichbar bzw. globalisiert werden, d.h. es kann Objekten auf entfernten Hosts ermöglicht werden, entfernte Verweise auf dieses Objekt zu haben. Dazu muß die entsprechende Klasse mit dem Tag globalizable versehen werden. Nur Instanzen solch einer Klassen können by-refvalue als Argumente an entfernte Methoden übergeben werden. Wird ein Objekt by-refvalue übergeben, so erfolgt der Zugriff auf seine Instanzvariablen grundsätzlich in derselben Art, wie auf das Objekt selbst, also gegebenenfalls entfernt. Dazu brauchen die Instanzvariablen nicht mit einem Tag versehen zu sein.
Jede Klasse kann globalizable gekennzeichnet werden, auch Klassen, die z.B. von Systemklassen wie Hashtable erben. Auf diese Weise kann ein entfernt referenzierbarer Hashtable erzeugt werden. Die Eigenschaft globalizable wird grundsätzlich von einer Überklasse "geerbt", d.h. übernommen, sofern diese ebenfalls globalizable ist.
Je zwei Verweise auf dasselbe globalisierte Objekt sind gleich, so daß der == Operator true liefert, egal wo das Objekt liegt (im Gegensatz zu RMI [SUN98]). Verweise auf entfernte Objekte werden vom Runtime System automatisch von Host zu Host zum aktuellen Aufenthaltsort des Objekts verfolgt, ohne daß sich der Host, von dem aus der Verweis los wandert, darum kümmern muß.
Wenn ein Objekt by-copy an eine entfernte Methode übergeben werden soll, muß seine Klasse mit dem Tag Primitive Datentypen sind von vornherein Anwendung der Tags: Objekte by-refvalue oder by-copy übergeben
Zunächst werden alle Klassen mit dem Tag globalizable versehen, auf deren Instanzen fern zugegriffen werden soll. Interfaces können auch globalizable gekennzeichnet sein, dann haben alle implementierenden Klassen diese Eigenschaft. Ein Fernzugriff ist erforderlich, falls
Beispiel:
Des weiteren werden Klassen bzw. Interfaces mit dem Tag copyable markiert, falls die entsprechenden Objekte als Kopie übergeben werden sollen:
Jetzt ist für die Instanzen der oben markierten Klassen spezifiziert, ob sie by-refvalue oder by-copy übergeben werden können. Wie sie tatsächlich übergeben werden sollen, muß aber mit den Tags by-copy und by-refvalue noch angegeben werden. Argumente und Rückgabewerte für die fernaufrufbaren Methoden, d.h. für die Methoden der globalisierbaren Klassen und Instanzvariablen, auf die entfernt zugegriffen werden soll, werden mit entsprechenden Tags versehen. Beim entfernten Zugriff braucht vom Programmierer nicht beachtet werden, auf welchem Host die Instanzen liegen, das erledigt das Doorastha Runtime System.
Bei lokalen Aufrufen von Methoden aus globalisierten Klassen werden diese Tags nicht berücksichtigt, die Semantik ist dann wie bei lokalen Methoden, d.h. ohne Effizienzverlust. Fernaufrufbare Methoden müssen nicht public sein (im Gegensatz zu RMI [SUN98]). Methoden und Variablen von globalizable Klassen, auf die nie entfernt zugegriffen werden soll, können zum Effizienzgewinn mit dem Tag ignore gekennzeichnet werden.
Für an entfernte Hosts by-copy übergebene Objekte können für die einzelnen Objektvariablen der Unterobjekte auch die Übergabeeigenschaften bestimmt werden. Zusätzlich zu den Tags by-copy und by-refvalue kann der Tag null verwendet werden, um die Variable zu ignorieren, genauer ihren Wert nach der Übergabe mit null zu belegen.
Mit rebind kann eine Variable nach dem Kopieren des Objekts, zu dem sie gehört, neu initialisiert werden.
Entfernte Erzeugung von Objekten
Objekte können nicht nur lokal mit dem new Operator erzeugt werden, sondern auch entfernt mit dem Tag remotenew. Im folgenden Beispiel wird auf einem entfernten Host ein Thread erzeugt. Klassen, deren Instanzen entfernt erzeugt werden sollen, müssen globalizable sein, da auf die fern erzeugte Instanz zugegriffen werden soll. Die Klasse Thread selbst ist zwar nicht globalizable, der erzeugte Thread gehört aber zu einer Unterklasse von Thread, die als anonyme Klasse globalisiert wird.
Klassenvariablen und Klassenmethoden
Klassenvariablen und -methoden (static) sollen für alle Instanzen einer Klasse, bzw. in allen Vorkommen einer Klasse auf verschiedenen Hosts einheitlich sein. Dies kann u.U. nicht garantiert werden, wenn die Klasse auf andere Hosts kopiert und die Klassenvariablen später verändert werden. Daher sollte eine Klassenvariable oder -methode auf einem bestimmten Host liegen und alle Zugriffe dorthin geleitet werden. Dies kann mit dem Tag unique veranlaßt werden.
Bei Client-Server-Architekturen werden Server und Client logisch voneinander getrennt auf verschiedenen Rechnern gestartet, wobei der Client versucht, zum Server Kontakt aufzunehmen. Dazu bietet RMI die Möglichkeit, einen Namensdienst auf dem Serverrechner zu starten, der über eine IP-Adresse und einen Port gesucht werden kann und gegen einen String ein entferntes Objekt an den Client liefert. Der Client kann dabei unabhängig vom Server zu einem beliebigen Zeitpunkt (nach dem Server) gestartet werden. Auf Client- und Serverseite werden verschiedene JVM’s gestartet.
Ein Doorastha Programm wird für gewöhnlich von einem Rechner aus, d.h. in einer JVM, gestartet und migriert Teile zu anderen Rechnern, auf denen ein Java Spawnserver (in einer JVM) laufen muß. Dieser initialisiert dann die Programmteile.
Es ist jedoch unter Doorastha auch möglich, von der Client-Seite aus den Server zu kontaktieren, obwohl Doorastha nicht die Benutzung des RMI-Namensdienstes vorsieht, da solche Details für den Programmierer verborgen sein sollen. Dazu kann das Serverobjekt vom Server in einer Klassenvariablen gespeichert werden, die mit dem Tag unique auf den Serverhost lokalisiert wird. Der Client kann den Server einfach über diese Variable referenzieren:
Doorastha erlaubt die Synchronisation von Threads über einen Monitor, auch wenn die Threads auf verschiedenen Rechnern liegen. Falls nur synchronized Methoden verwendet werden, kann dieselbe Syntax wie im zentralisierten Fall verwendet werden. Wird ein synchronized Block über einem globalisierten Objekt als Monitor verwendet, so muß die zugehörige Klasse mit dem Tag synchronizable versehen sein.
Ob ein Objekt by-refvalue oder by-copy übergeben werden soll, ist normaler Weise statisch durch das Tag by-refvalue bzw. by-copy an der bezeichnenden Variablen bestimmt (und muß zum Tag globalizable bzw. copyable der zugehörigen Klasse passen). Die Übergabeart kann aber auch dynamisch erst zur Laufzeit festgelegt werden, z.B. wenn zwischen zwei Rückgabewerten mit unterschiedlichen Tags entschieden werden soll.
Im folgenden Beispiel wird ein Fabrik-Interface implementiert. Die implementierenden Klassen können selbst die Übergabeart bestimmen. Objekte, die sich selten ändern, aber oft referenziert werden, sollten z.B. lieber by-copy übergeben werden.
Migration: Wandernde Objekte und Threads
Neben der by-refvalue und by-copy Übergabe von Argumenten und Rückgabewerten bei entfernten Methodenaufrufen kann der Parameter auch auf den entfernten Host verschoben werden. Der Effekt ist in diesem Fall kein dupliziertes Objekt, sondern ein auf dem neuen Host lokal vorhandenes, also nicht entfernt referenziertes Objekt. Dazu muß die Klasse des Objekts den Tag migratable und der Parameter oder die Variable, die das Objekt bezeichnet, den Tag by-move haben. Außerdem muß das Objekt mit dem Tag remotenew erzeugt worden sein. Objekte mit dem Tag migratable sind automatisch globalizable und copyable.
Auch aktive Objekte, genauer Threads können zwischen den Hosts verschoben werden, z.B. um mobile Agenten zu bauen. Bei der schwachen Form von Mobilität ist auf dem neuen Host explizit Programmcode vorzusehen, um den Agenten "zum Leben zu erwecken", d.h. den Thread neu zu starten. Das folgende Beispiel läßt einen Thread sich auf einem anderen Host selbst neu entstehen, d.h. er migriert dorthin, erzeugt einen neuen Thread mit seinen Eigenschaften (seiner aktuellen counter Variable) und beendet sich auf dem alten Host.
Es ist mit Doorastha aber auch ohne großen Aufwand möglich, einen aktiven Thread auf einen anderen Host zu verschieben. Dabei erwacht der migrierte Thread in demselben Zustand (derselben Programmzeile) wie er verschickt wurde, ohne daß dazu spezieller Programmcode nötig ist. Dazu muß von der in Doorastha enthaltenen Klasse MigratableThread geerbt werden. Die Methode migrate() innerhalb der run() Methode läßt den Thread anhalten, zum angegebenen Host migrieren und dort die Arbeit in der nächsten Programmzeile wieder aufnehmen. Die lokalen Variablen in der run() Methode können auf die bekannten verschiedenen Arten übergeben werden:
Eine verkettete Liste mit großen Datenobjekten wird zwischen verschiedenen Objekten auf verschiedenen Hosts "umher gereicht" und dort jeweils traversiert. Daten- und Traversierobjekte können auf verschiedenen Hosts beheimatet sein und müssen daher entfernt erreichbar, d.h. globalizable sein. DataObject erbt die Zugriffsmethoden von Hashtable (extends Hashtable), die nun lokal und entfernt aufrufbar sind.
Die Methode check() der Klasse Traverser macht einen Gleichheitstest, für den sie intern den == Operator verwendet. Doorastha garantiert die erwartete Semantik, auch wenn ein Argument von einem anderen Host kopiert wird (was bei anderen verteilten Systemen nicht möglich ist!). Die Traversierung traverse() einer großen Liste wäre sehr ineffizient, wenn die dazu benötigte Kommunikation mit der inneren Klasse Iterator der entfernten Klasse List über das Netz laufen würde. Daher wird die Liste bei der Übergabe an traverse() kopiert.
Da der eigentliche Datenbestand der Liste, d.h. die DataObject Objekte sehr groß sind, sollen diese nicht kopiert werden (by-refvalue). Nur die Struktur der Liste wird kopiert, insbesondere die Node Objekte (copyable, by-copy), nicht aber die darin enthaltenen DataObject Objekte. Der Vorgang der Traversierung ist nun eine lokale Operation auf dem Host, auf den die Liste kopiert wurde. (Die Methode iterator() wird lokal aufgerufen und braucht daher kein Tag.) Nur der Zugriff auf den eigentlichen Datenbestand läuft über das Netz. Man bemerke, das Programm ist auch in der zentralisierten Version ohne Tags, in der es zunächst entworfen wurde, funktionstüchtig!
Hinter den Kulissen von Doorastha: Entwurf mit Java RMI
Doorastha bedient sich der Möglichkeiten von RMI [SUN98], womit Objekte als Netzwerkreferenzen und Kopien durch Serialisierung [Jav98b] übergeben werden können. Doorastha ermöglicht, ein Programm verteilungstransparent zu schreiben, d.h. der Programmierer muß keine Details bezüglich der Verteilung berücksichtigen, sondern diese nur durch wenige Schlüsselwörter in seinem zentralisierten Programm anstoßen. Das Doorastha-System erzeugt automatisch einen RMI konformen Code, indem die erwähnten Schlüsselwörter in zusätzlichen Code umgesetzt und einige neue Klassen erzeugt werden.
Eine Instanz einer globalisierbaren Klasse kann für Zugriffe von entfernten Hosts verfügbar gemacht werden. Diese sogenannte Globalisierung wird vom Doorastha-System dynamisch während der Laufzeit vorgenommen. Zunächst müssen hinter den Kulissen für globalisierbare, d.h. mit dem Tag globalizable gekennzeichnete Klassen weitere Klassen erzeugt werden. Der Hauptteil des Programmcodes für die Fernzugriffe, d.h. der neue Code, befindet sich im Wesentlichen in sogenannten Proxy und Wrapper Klassen. Der funktionelle Code für die zentralisierte Programmsemantik befindet sich in den alten Klassen. Für jede Klasse mit dem Tag globalizable werden zwei Hilfsklassen und ein Interface von Doorastha generiert, außerdem veranlaßt Doorastha das Java RMI System, zwei Klassen mit Hilfe des rmic Compilers zu erzeugen:
RMI [SUN98] erfordert die Generierung von Interfaces mit allen entfernt aufrufbaren Methoden. Für jede globalisierbare Klasse wird daher ein RMI Interface erzeugt. Diese Interfaces sind in einer Vererbungshierarchien angeordnet, die der der globalisierbaren Klassen entspricht. Diese erben letztendlich von der Klasse Object. Oben in der Vererbungshierarchie der Interfaces steht daher ein Interface Object_Interface für die Klasse Object mit allen public Methoden von Object. Diese müssen auch in einem Interface bereitgestellt werden, da die globalisierbaren Klassen von Object erben und diese Methoden somit auch entfernt aufgerufen werden können. Object_Interface erbt vom RMI Interface Remote, von welchem alle RMI-Interfaces erben müssen.
Wrapperklassen verbinden entfernt aufrufbare Klassen mit der Netzumwelt. Sie unterliegen der gleichen Vererbungshierarchie und implementieren die entsprechenden Interfaces. Für jede Instanz einer globalizable Klasse gibt es ein Wrapperobjekt, das sie gewissermaßen umhüllt bzw. ihr entspricht. Dieses Wrapperobjekt registriert sich beim RMI [SUN98] Runtime System und leitet alle vom Netz kommenden Zugriffe auf das Originalobjekt weiter, wozu es einen Verweis auf dieses Objekt hat. Ganz oben in der Vererbungshierarchie der Wrapperklassen steht die RMI Klasse UnicastRemoteObject, direkt darunter die Klasse Object_Wrapper als Wrapper für die Klasse Object.
Ein entfernt erreichbares Objekt braucht auf dem Host, von dem es aufgerufen wird, ein Stellvertreterobjekt. Dieses ist Instanz einer Proxyklasse, die direkt von der Klasse des eigentlichen Objekts erbt. Das Stellvertreterobjekt wird einfach anstelle des Originalobjekts von anderen Objekten referenziert, wobei seine Funktion darin besteht, die Zugriffe über das Netz an das Originalobjekt weiterzuleiten. Dafür müssen alle Methoden, die auf diesem Objekt aufgerufen werden können, mit einer Netzwerkfunktionalität überschrieben werden. Wenn Variablen oder Methoden mit dem Tag ignore gekennzeichnet sind, wird für sie kein Code im Proxyobjekt erzeugt.
Zur Übergabe von kopierten Objekten werden diese zunächst serialisiert [Jav98b]. Dabei wird das gesamte Objekt mit Name, Inhalten seiner Variablen und, soweit erforderlich, allen transitiv referenzierten Unterobjekten in einen Datenstrom geschrieben. Zur Serialisierung werden die Methoden writeObject() und readObject() der Klasse ObjectInputStream aus der Java Standardbibliothek verwendet. Um serialisierbar zu sein, muß eine Klasse das Interface Serializable implementieren. Das passiert implizit bei Klassen mit dem Tag copyable.
Migrierbare Klassen werden im Prinzip mit den Mechanismen des Kopierens und Globalisierens behandelt, wobei ein Verweis auf das Objekt zurückbleiben muß.
Fernzugriffe unterscheiden sich gegenüber lokalen Zugriffen unter Doorastha im Programmcode im Prinzip nicht, verteilte Programmierung erscheint transparent. Für das RMI System gibt es aber wesentliche Unterschiede, die teilweise erst zur Laufzeit behandelt werden können. Ob die aktuellen Parameter eines Methodenaufrufs mit der Signatur der Methode übereinstimmen, wird, so weit möglich, schon vom Compiler getestet. Zur Laufzeit werden Objekte dann bei Bedarf dynamisch globalisiert oder kopiert, falls die betreffenden Verweise auf einen entfernten Host zeigen. Wird eine Methode an einem Objekt aufgerufen, so sind z.B. beim Aufrufer bezüglich der Methodenargumente nur dann besondere netzbezogene Maßnahmen zu ergreifen, falls das Objekt entfernt ist und das Argument auf dem Host des Aufrufers liegt. In allen anderen Fällen kann mit dem aufgerufenen Objekt wie beim normalen zentralisierten Java verfahren werden, d.h. ohne Netzaktivitäten zu starten. Beim Aufgerufenen werden im Falle eines entfernten Aufrufers z.B. Verweise auf Argumente, die auf dem Host des Aufgerufenen liegen in lokale Verweise umgewandelt. Diese Verweise sind dann nur für den Aufrufer von entfernter Natur.
Innere Funktionsweise des Doorastha-Systems und Implementierung: Nur soviel Netzwerkaktivität wie nötig
Ein Objekt, welches by-refvalue an einen entfernten Host übergeben werden soll, wird vom Doorastha-System globalisiert, d.h. entfernt verfügbar gemacht, indem intern die sogenannte globalize() Methode des Objekts aufgerufen wird. Diese Methode wurde, ebenso wie ihr Aufruf, vom Doorastha-System für den Programmierer unsichtbar aufgrund der verwendeten Schlüsselwörter erzeugt. globalize() erzeugt zur Laufzeit eine Instanz der zugehörigen Wrapperklasse (Hülle) auf dem Host des Objekts, die sich beim RMI Runtime System registriert. Dann erzeugt die Methode ein Proxyobjekt (Vertreter), das einen RMI Verweis auf den Wrapper hat. Dieses Proxyobjekt wird anstelle des Originalobjekts über das Netz gesendet und wird von nun an Anfragen an das nun entfernte Originalobjekt, über den Wrapper, weiterleiten. Das Proxy vertritt sozusagen das Originalobjekt. Ein lokaler Verweis auf ein Proxy stellt einen entfernten Verweis auf das Originalobjekt dar. Die Methoden des Objekts einschließlich aller geerbter Methoden sind im Proxy überschrieben und leiten Aufrufe weiter an das entfernte Originalobjekt. Dazu sind RMI spezifisch Methoden, die in Interfaces aufgeführt sind, nötig. Sie tragen einen modifizierten Namen (siehe Abbildung 1).
Die by-copy Übergabe von Argumenten für Methodenaufrufe erfolgt mit der RMI Serialisierung [Jav98b]. Für den Zugriff auf entfernte Variablen globalisierter Klassen generiert Doorastha Zugriffsmethoden, die wie oben beschrieben arbeiten.
Doorastha überprüft bei der Erstellung von entfernten Verweisen, ob diese durch lokale Verweise ersetzt werden können. In diesem Fall brauchen sie nicht über das RMI System zu laufen. Zu diesem Zweck führt das Doorastha-System auf allen Hosts eine Tabelle mit allen schon globalisierten Objekten dieses Hosts. Wenn ein bereits globalisiertes Objekt X by-refvalue an ein anderes Objekt Y auf einem entfernten Host übergeben wird, so geprüft das Doorastha Runtime System dort, ob das Original X dort beheimatet ist und modifiziert gegebenenfalls den neuen Verweis von Y auf X in einen lokalen Verweis. Wenn das Objekt entfernt ist, aber schon ein Proxyobjekt dafür existiert, so wird dieses weiter benutzt ohne ein neues zu erzeugen.
Da alle Verweise auf ein Objekt auf einem Host stets bei demselben Proxy oder, falls dort vorhanden, Original landen, ist die richtige Semantik des == Operators sichergestellt. Zwei Vertreter desselben Objekts würden eine Ungleichheit ergeben. Ebenso verweisen alle Proxies eines Objekts auf dasselbe entfernte Wrapperobjekt.
Bisher wurde die Frage behandelt, wie auf ein entferntes Objekt zugegriffen wird. Dazu muß es aber erst einmal auf einen entfernten Host gelangen oder dort erzeugt werden. Um auf einem entfernten Host ein Objekt zu erzeugen, wird dort eine Fabrik benötigt. Diese Funktionalität übernimmt die zum Objekt gehörige Wrapperklasse auf dem entfernten Host. Zunächst wird die Zeile
beim Compilieren übersetzt in
Beim Ausführen dieser Programmzeile wird die Klassenmethode createA() der zu dem zu erzeugenden Objekt gehörigen Proxyklasse aufgerufen.
createA()
erstellt eine entfernte Referenz auf die Fabrik auf dem entfernten Host mit Hilfe des dortigen Doorastha Runtime Systems. Auf diesem Fabrikverweis wird die createA() Methode der entfernten Fabrik aufgerufen, die die Aufgabe des Konstruktors der Klasse A übernimmt und dort ein Objekt der Klasse A erzeugt und einen Wrapper installiert. Als Rückgabewert der entfernten Methode wird ein Proxy über das Netz zurückgegeben, der als lokale Referenz in der Variablen a gespeichert wird. Die Zuweisung eines A_Proxy Objektes zur Variablen A a ist möglich, da A_Proxy von A erbt.Die by-copy Übergabe von kopierten Objekten mitsamt ihrer Unterobjekte wird vom RMI [SUN98] System automatisch vorgenommen. Doorastha versieht dazu Klassen, die mit dem Tag copyable gekennzeichnet sind, mit dem Zusatz implements Serializable. Nur bei Klassen, die Variablen enthalten, die by-refvalue übergeben werden sollen, muß die writeObject() und readObject() Methode des Serialisierungsmechanismus [Jav98b] überschrieben werden, damit die betreffenden Objekte nicht serialisiert sondern globalisiert werden. Ein gerade kopiertes Objekt kann nur durch lokale Referenzen erreicht werden.
Objekte können bei Übergabe an entfernte Methoden migrieren. Aber auch eine explizite Aufforderung im Programm kann dazu führen. Dabei kann eine Bedingung angegeben werden:
Um auf andere Hosts migrieren zu können, muß ein migratable Objekt mit dem Tag remotenew erzeugt worden sein. Dies führt dazu, daß alle Referenzen auf das Objekt, und zwar leider auch lokale Referenzen, grundsätzlich über ein Proxy und Wrapper laufen. Wären lokale direkte Referenzen möglich, so würden diese beim Verschieben des Objekts auf einen anderen Host verloren gehen. So aber sammelt der zugehörige Wrapper alle Referenzen auf. Ein migrierbares Objekt behält seine Identifizierungsnummer, die seinen Geburtsort enthält, immer bei, damit seine Proxies ihm zugeordnet werden können, was wichtig für Vergleiche ist.
Zum Migrieren wird ein Objekt im Prinzip kopiert, wobei seine Fabrik die Netzwerkanbindung mit dem Wrapper erstellt. Der alte Wrapper erhält die Information über den neuen Host und teilt sie bei einem Aufruf mit einer Exception dem aufrufenden Proxy mit. Der Proxy versucht nun den gleichen Aufruf mit der neuen Adresse. Dieses wird solange fortgesetzt, bis das Objekt, das mittlerweile ja weiter migriert sein kann, gefunden ist.
Die Migration von laufenden Threads ist nicht in reinem Java implementiert, sondern wird mit byte code engineering Technik [Dah99] ausgeführt. Folgender Pseudocode stellt das Prinzip der Migration dar. Aus folgendem Quelltext
wird sinngemäß
Wenn im laufenden Thread an einer Stelle eine gewisse Bedingung erfüllt ist, werden alle lokalen Variablen der run() Methode gespeichert, migrate() aufgerufen und der aktuelle Thread durch Sprung aus der for-Schleife terminiert. migrate() verschiebt das Thread Objekt auf einen anderen Host und startet ihn dort durch Aufruf von run(). Am Anfang von run() wird geprüft, ob der Thread gerade migriert ist. In diesem Falle wird zunächst ein Codestück zum Speichern der lokalen Variablen angesprungen, welches sonst nie ausgeführt wird.
Ob ein Objekt by-copy oder by-refvalue an eine Methode übergeben werden soll, kann auch dynamisch erst zur Laufzeit festgelegt werden. Dafür muß das Doorastha-System beim Compilieren verschiedene Codetransformationen vornehmen. Da die Übergabeart nicht mehr statisch behandelt werden kann, müssen zur Laufzeit allerhand Überprüfungen vorgenommen werden.
Klassenvariablen und –methoden können mit dem Tag unique versehen werden, damit sie im verteilten System nur einmal an im Tag definierter Stelle existieren, d.h. alle Anfragen zu demselben Host geleitet werden. Doorastha generiert für alle Lese- und Schreibzugriffe gemäß dem Tag Aufrufe für spezielle set/get Methoden. Diese werden in der Fabrik der Klasse, zu der die Klassenvariable gehört, entfernt ausgeführt.
Werden Threads über einem globalisierten Objekt synchronisiert, so wird dieses Monitorobjekt während der Laufzeit von einem lokalen Proxy vertreten. In folgendem Beispiel wäre ein Thread, der eine Methode mit einem synchronized Block aufruft, von einem Deadlock bedroht, denn möglicher Weise wird notify() auf einem anderen Proxy (vom selben Monitorobjekt) aufgerufen als wait() und nicht jeder Thread benachrichtigt.
Wenn nur synchronized Methoden eines globalisierten Objekts verwendet werden, so werden die Aufrufe jeweils zum Originalobjekt, auf dem sie aufgerufen wurden und das den Monitor darstellt, weitergeleitet und alle Threads, die dort z.B. ein wait() aufgerufen haben, werden benachrichtigt.
Die Klasse mit synchronisierenden Anweisungen wie oben muß das Tag synchronizable tragen. Doorastha wandelt diese Anweisungen in Methodenaufrufe wie folgende um:
Diese Aufrufe werden auf den Host mit dem Originalobjekt umgeleitet. Dort gibt es für alle Thread-Monitor-Paare ein Schloßobjekt, das diese Methoden ausführt, z.B. auf ein notify eines entfernten Threads zu warten.
Das Doorastha Runtime System verwaltet die netzwerk- bzw. RMI-spezifischen Belange des laufenden verteilten Programmes. Es unterhält Tabellen der globalisierten Objekte, unterhält die zugehörigen Proxies, verwaltet das Migrieren von Objekten und ihre Fabriken.
RMI [SUN98] benötigt eine spezielle Registry auf jedem beteiligten Host. Diese kann mit dem Kommando rmiregistry oder in einem Thread in einer Java Applikation gestartet werden. Dies übernimmt das Doorastha Runtime System.
Der User sollte ein Programm mit dem doova Skript starten, um das Doorastha-System automatisch miteinzubeziehen.
Alle Ausgaben nach System.out werden auf die Konsole des Hosts, auf dem das Hauptprogramm gestartet wurde, umgeleitet.
Das Doorastha Übersetzungssystem basiert auf dem Barat System [BS98], welches Java Klassen statisch analysieren kann. Dabei kann der Syntaxbaum traversiert und Kommentare von der Form /*:...*/ ausgewertet und demzufolge Programmcode an den Knoten des Baumes integriert werden. Die Doorastha Tags werden in Form dieser Kommentare verwendet, so daß das Programm auch mit einem normalen Java Compiler übersetzt und als zentrale Version laufen kann. Die Erzeugung von Byte Code erfolgt mit Hilfe des JavaClass Systems [Dah98]. Dabei werden bestimmte Attribute, die Doorastha Tags, an den Knoten des Syntaxbaumes vom Programmierer verwendet. Diese Attribute können im Prinzip auch von automatischen Verteilungswerkzeugen wie Pangaea [Spi99] erzeugt werden. Der Doorastha Compiler doovac parst die Kommentare, fügt mit Barat die entsprechenden Attribute an die zugehörigen Knoten an, überprüft die syntaktische Konsistenz, generiert Interfaces, Proxies und Wrapper und erzeugt mit JavaClass Byte Code.
Literatur
Markus Dahm, The Doorastha-System, Technical Report B-1-2000, April 4, 2000 http://www.inf.fu-berlin.de/~dahm/doorastha
[BS98] B. Bokowski and A. Spiegel. Barat A Front-End for Java. Technical report, Freie Universität Berlin, 1998.
[Dah99] M. Dahm. Byte Code Engineering. In Clemens Cap, editor, Proceedings JIT’99. Springer, 1999.
[Jav98b] JavaSoft. Serialization API. http://java.sun.com/products/jdk/1.2/docs/guide/serialization/, 1998.
[Spi99] André Spiegel. Pangaea: An Automatic Distribution Front-End for Java. In HIPS’99, 1999.
[Sun98] Sun Microsystems. Java Remote Method Invocation Specification. http://java.sun.com/products/jdk/1.2/docs/guide/rmi/, February 1998.