Bei der Implementation der Programmiersprache Emerald wurden die Fehler der oben aufgeführten Ansätze berücksichtigt, und es wurde versucht, diese nicht zu wiederholen. Es wurden mehrere Grundsätze festgelegt, denen die Sprache genügen sollte [2]:
Einzelne dieser Punkte sind an sich nichts neues. Smalltalk besitzt das einheitliche Objektmodell, in EPL ist das Netzwerk transparent, und prozedurale Programmiersprachen wie C++ bieten Effizienz beim Objektzugriff [11]. Die feinkörnige Mobilität einzelner Objekte jedoch wird mit Emerald erstmals propagiert.
Bei der Implementation wurde auf die Effizienz besonderen Wert gelegt. In vielen Systemen werden erst andere Ideale verwirklicht, dann wird an der Effizienz gefeilt. Die Entwickler haben herausgestellt, daß Performance nicht nachträglich geschaffen werden kann; sie muß von Anfang an geplant werden. Es wird propagiert, die Effizienz von Regelfällen zu optimieren, auch wenn dies zusätzliche Probleme in Sonderfällen bereitet. Die aufwendigere Zusatzbehandlung der Sonderfälle wird oftmals durch den Zeitgewinn im Regelfalle mehr als begründet.
Objektorientierte Sprachen wie Smalltalk und das im Objektkonzept von Simula abstammende C++ haben stets das Konzept von Klassen verfolgt. An einer Stelle wird die Klasse als Schablone definiert, an anderer Stelle können mit dem Operator new Instanzen dieser Klasse erzeugt werden. Dem zugrunde liegt die Idee, daß sich gleich verhaltende Objekte in einer Klasse zusammenfassen lassen können, so daß das Verhalten nur an einer Stelle beschrieben werden muß.
Emerald folgt einer anderen Philosophie, in der jedes Objekt durch die Ausführung eines Konstruktors erzeugt wird. Alle Eigenschaften des Objektes, wie lokale Daten, Definition der Schnittstelle und der Programmtext der Operationen sind Teil des Konstruktors. Bei der Ausführung des Konstruktors entsteht das beschriebene Objekt.
Dies hat natürlich den Effekt, daß zunächst bei der Ausführung eines Konstruktors nur genau ein einziges Objekt erzeugt wird. Da es keine Möglichkeit zum Kopieren gibt, muß zur Herstellung mehrerer Objekte vom gleichen Typ der Konstruktor in eine Schleife gestellt werden, oder es muß ein übergeordnetes Objekt mit der einzigen Intention definiert werden, neue Objekte des untergeordeten Typs zu erzeugen, was natürlich im Programmtext ziemlich häßlich aussieht. Hier ein Beispiel für ein Objekt-Erzeuger-Objekt:
const IntegerNodeCreator <- immutable object INC export new const IntegerNodeType <- type INType function getValue -> [Integer] operation setValue [Integer] end INType operation new[val : Integer] -> [aNode : IntegerNodeType] aNode <- object IntegerLiteral export getValue, setValue monitor var value <- val operation getValue -> [v : Integer] v <- value end getValue operation setValue [v : Integer] value <- v end setValue end monitor end IntegerLiteral end new end INC
Wie beschrieben ist ein Konstruktor der einzige Weg um ein Objekt zu erzeugen. Bei der Konstruktion wird für jedes Objekt eine netzwerkweit eindeutige Identifikation generiert. Es ist sichergestellt, daß sich ein Objekt immer genau auf einem Rechner befindet, also eine feste Lokation besitzt. Alle Zugriffe auf ein Objekt erfolgen über Referenzen; bei jeder "Weitergabe" des Objektes, z.B. in einem Prozeduraufruf, wird nur eine weitere Referenz auf das Objekt erzeugt. Die Tatsache, daß ein Objekt nicht kopiert wird, erspart ansonsten unvermeidbare Konsistenzprobleme.
Ein Objekt kann zusätzlich zu der Beschreibung seiner Schnittstelle einen optionalen Prozeß enthalten. Dieser wird bei der Ausführung des Objektkonstruktors asynchron gestartet, und läuft parallel zu anderen Prozessen. Ein Objekt mit Prozeß wird als aktiv bezeichnet, im Gegensatz zu passiven Objekten ohne Prozeß. Der Prozeß darf seinerseits nach belieben neue Objekte erzeugen, andere Objekte aufrufen etc. Diese Variante des Multithreading fordert Synchronisationsprimitiven, da mehrere Prozesse gleichzeitig auf ein Objekt zugreifen können.
Im Gegensatz zu vereinbarter Synchronisation mittels Semaphoren sind Objekte in Emerald in einen bewachten (monitored) und einen unbewachten Teil unterteilt. Prozeduren innerhalb des bewachten Teils haben exklusiven Zugriff auf die Variablen im bewachten Teil, und Prozeduren des unbewachten Teils ist es nicht erlaubt, auf bewachte Variablen zuzugreifen. Im obigen Beispiel befinden sich die Operationen setValue und getValue sowie die Variable val im bewachten Teil des Objektes. Das äußere Objekt INC hat keinen bewachten Teil, da die Operation new unkritisch ist. Der Programmierer muß jedoch selber dafür sorgen, daß alle kritischen Operationen innerhalb eines bewachten Teils stattfinden.
Falls ein Objekt keinen inneren Zustand hat, oder sich über die Zeit hinweg nicht verändert, so kann der Programmierer das Objekt als unveränderlich (immutable) deklarieren. Prozeduren eines unveränderliches Objektes erzeugen bei Eingabe bestimmter Werte stets den gleichen Rückgabewert, sie sind also Funktionen im mathematischen Sinne. Der Sinn dieser Deklaration ist, daß das System bei einem unveränderlichen Objekt weiß, daß es keine Konsistenzprobleme mit Kopien geben kann. Falls auf ein entferntes unveränderliches Objekt zugegriffen wird, so kann dieses einfach auf den lokalen Rechner kopiert werden. Prinzipiell sind unveränderliche Objekte auf allen Rechnern lokal, und überall kann daher mit lokalen Mechanismen auf das Objekt zugegriffen werden.
Emerald unterstützt Polymorphismus über die anfangs erwähnte Konformitätsrelation [3]. Falls sich ein Objekt zu einem anderen Typkonform verhält, so kann es auch den anderen Typ annehmen. Andererseits erlaubt Emerald keine Vererbung und deshalb auch keine Überladung von Eigenschaften. In typkonformen Objekten muß deshalb der Programmcode gleicher Prozeduren mehrmals aufgeführt werden.
In Emerald existieren 4 Befehle, um Objekte zu bewegen. Zwar ist eine völlige Transparenz des Netzwerkes in vielen Fällen von Vorteil, in anderen Fällen ist es allerdings auch nützlich, eine Kontrolle über die Lokation von Objekten zu haben, z.B. um das System bei der Ressourcenverteilung zu unterstützen. Der Programmierer verteilter Anwendungen kann mit diesen Primitiven helfen, die Anzahl entfernter Objektaufrufe zu minimieren [2].
Der Befehl move ist ziemlich schwach, das System ist noch nicht einmal verpflichtet, das Objekt zu bewegen. Überhaupt ist der Befehl eine ziemlich unsichere Sache. Während der Ausführung eines move könnte sich Y beispielsweise selber bewegen, oder ein zweiter move-Befehl könnte X sofort an einen anderen Rechner weiterleiten.
Die restlichen drei Befehle sind stärker, allerdings muß der Programmierer auch hier mehrere Fälle bedenken. Zwar kann X auf den Rechner von Y fixiert werden, doch Y kann sich noch immer bewegen. Erst die Kombination "fix Y at Y, fix X at Y" stellt sicher, daß sich anschließend beide Objekte auf dem gleichen Rechner befinden.
Bei der Bewegung eines Objektes muß auch entschieden werden, wieviel bewegt werden soll. Offensichtlich ist es nicht sinnvoll, nur das einzelne Objekt auf den neuen Rechner zu verschieben. Falls dem bewegten Objekt lokale Variablen nicht mitbewegt werden, so erfordert jeder weitere Zugriff einen aufwendigen entfernten Prozeduraufruf. Lokale Variablen, die nicht außerhalb eines Objektes verändert werden, können daher unmittelbar mitbewegt werden.
In anderen Fällen ist die Entscheidung schwieriger, z.B. bei Objekten, die dem bewegten Objekt nahestanden, d.h. zwischen denen viele Daten ausgetauscht werden. Soll das zweite Objekt bei der Bewegung gleich mitgeschickt werden, oder wird das erste Objekt beim nächsten Aufruf wieder zurückgeholt? Emerald bietet dem Programmierer das Konzept der Zugehörigkeit, um die Relationen zu beschreiben. Eine Variable kann als zugehörig (attached) definiert werden. Wird ein Objekt bewegt, so folgen ihm alle Objekte, die mit einer zugehörigen Variable referenziert werden.
Wie bereits erwähnt werden bei einem Prozeduraufruf nur Referenzen eines Objektes übergeben. Falls sich das aufrufende und das aufgerufene Objekt auf verschiedenen Rechnern befinden, hat das zur Folge, daß das aufgerufene Objekt für jeden Zugriff auf die Parameter wieder einen entfernten Zugriff ausführen muß (Grafik 2 oben). Analog befindet sich das Ergebnis der Prozedur bezüglich dem lokalen Objekt auf einem entfernten Rechner, und die aufrufende Prozedur muß stets mit entfernten Mechanismen auf den Rückgabewert zugreifen. Es kann von Vorteil sein, die Prozedurparameter beim Aufruf auf den entfernten Rechner und die Rückgabewerte auf den Rechner des Aufrufers zu bewegen. In Grafik zwei wird eine einfache Situation dargestellt. Objekt X ruft eine Prozedur des Objektes Y auf, und übergibt das lokale Objekt Z als Parameter, welches von Y genau einmal verwendet wird. Falls Z nicht bewegt wird, so müssen vier rechnerübergreifende Sprünge vollzogen werden, falls Z mitbewegt wird, nur zwei.
Die Sprache stellt dem Programmierer drei Varianten des klassischen call-by-reference zur Verfügung. Alle Varianten sind optional, der Programmierer kann die Entscheidung auch dem Compiler oder dem Betriebssystem überlassen. Die Entscheidung, einen Parameter zu bewegen hängt primär von zwei Faktoren ab; einerseits von der Größe des Parameters, andererseits von der Anzahl der Zugriffe des aufgerufenen Objektes auf den Parameter.
Alle drei Varianten bewegen die entsprechenden Objekte mit dem move-Befehl, was bedeutet, daß die vom Programmierer beschriebenen Übergabemöglichkeiten nicht explizit befolgt werden müssen. Wiederum ist es dem Compiler und Betriebssystem freigestellt, auf die Bewegung zu verzichten.
Die Sprache besitzt noch einige aus der Verteiltheit resultierende Eigenschaften, die hier kurz Erwähnung finden sollten, insbesondere netzwerkbezogene Sprachmöglichkeiten, mit denen ein Programmierer auf die Dynamik des Netzes reagieren kann.
In verteilten Umgebungen können Rechner unabhängig voneinander ausfallen, und Objekte, die sich auf diesem Rechner befanden, werden unerreichbar. Ein Programmierer kann für jedes Objekt einen unavailable handler definieren, der bei dem Versuch, ein unerreichbares Objekt auszuführen, aufgerufen wird. Analog kann ein failure handler definiert werden, um Fehler wie division by zero, Stapelüberlauf oder at-runtime festgestellte ungültige Prozeduraufrufe abzufangen. Falls in einem Objekt ein Fehler auftritt, dieses jedoch keine Fehlerbehandlung definiert, so wird der Fehler so lange zurückgegeben, bis ein fehlerbehandelndes Objekt erreicht wird.
Eine Anwendung kann darüber hinaus einen NodeEventHandler installieren, der jedesmal aufgerufen wird, wenn ein Rechner des Netzwerkes ausfällt oder erneut verfügbar wird. Die Anwendung kann mit dieser Information flexibel auf Veränderungen reagieren, und Aufgaben auf neue Rechner auslagern, oder die Bewegung von Objekten auf unsichere Rechner verhindern.
[Inhaltsverzeichnis] [Voriges Kapitel] [Nächstes Kapitel]
Frank Pilhofer <fp -AT- fpx.de> Back to the Homepage Last modified: Fri Mar 31 18:41:07 1995