Leon Bornemann

Transcription

Leon Bornemann
Linda
Linda ist keine Programmiersprache im herkömmlichen Sinn, sondern kann als eine
Koordinationssprache betrachtet werden. Sie wurde von David Gelernter und Nicholas Carriero
Mitte der 80er Jahre entwickelt. Linda wurde bereits in C, Fortran, sowie java(Stichwort:
javaSpaces) implementiert.
Das Konzept von Koordinationssprachen
Gelernter und Carriero plädieren für eine strikte Trennung zwischen Programmiersprachen(engl.
computation languages) und Koordinationssprachen(engl. coordination langeuages). Erstere ist für
die Programmierung von sequenziellen Programmen zuständig, letztere für die Koordination von
nichtsequenziell ablaufenden Prozessen bzw. Programmen. Laut Gelernter und Carriero ist weder
eine "computation language", noch eine Koordinationssprache alleine brauchbar. Nur die
Zusammenführung von beiden erschafft eine vollständige Programmiersprache. Idealerweise sollten
dabei sowohl die "computation language", als auch die Koordinationssprache frei austauschbar sein.
Aufgrund dieses Konzeptes werden sämtliche Beispiele in dieser Zusammenfassung nur in
Pseudocode geschrieben, da die konkrete Syntax der "computing language" hier weniger wichtig
ist.
Linda: Konzept und Operationen
Linda arbeitet mit einer abgewandelten Form des message-passing. Das bedeutet es gibt keinen
gemeinsamen Speicher für Prozesse, sie müssen stattdesssen über Nachrichten miteinander
kommunizieren. Anders als beispielsweise bei Occam wissen die einzelnen Prozesse nichts von
einander, sie kommunizieren also nicht direkt miteinander. Folglich hat ein Prozess, der eine
Nachricht sendet, keine Ahnung welche Prozesse diese Nachricht lesen werden. Umgekehrt weiß
genauso ein lesender Prozess nicht, wer die Nachricht gesendet hat. Es ist also dem Programmierer
überlassen, für die Koordination der Nachrichten zu sorgen. Dies mag einem zunächst umständlich
erscheinen, doch dieses Konzept bietet viele Vorteile.
Alle Nachrichten, die in Linda gesendet werden, landen im Tupelraum(engl. "tuple space"). Diese
Nachrichten haben die Form von Tupeln. Um den Tupelraum zu manipulieren bietet Linda vier
Operationen:
-out(tuple)
Das spezifizierte Tupel wird erzeugt und in den Tupelraum gebracht. Beispiel:
out("helloWorld");
out(1337, "Linda", 42);
in(tuple)
Der Tupelraum wird nach dem spezifizierten Tupel durchsucht. Gibt es keines, so blockiert der
Prozess so lange, bis er eins findet. Gibt es mehrere der spezifizierten Tupel, so wird eines davon
zufällig ausgewählt. Das Tupel wird durch diesen Befehl aus dem Tupelraum entfernt. Beispiel:
in("helloWorld");
Der Befehl in(tuple) ermöglicht auch sogenannte Wildcards:
int var1;
int var2;
in(? var1, "Linda", ? Var2);
Es wird hier nach einem 3-Tupel gesucht, welches als an zweiter Stelle den String "Linda" zu
stehen hat und an erster und dritter Stelle zwei beliebige Ganzzahlen. Diese werden nun in den
Variablen var1 und var2 gespeichert.
rd(tuple)
rd steht für read, also das Lesen eines Tupels. Dieser Befehl funktioniert genauso wie in(tuple), nur
dass das gelesene Tupel nicht aus dem Tupelraum entfernt wird.
eval(tuple)
Dieser Befehl erzeugt einen Prozess, welcher Berechnungen durchführt, und anschließend(nach
seiner Terminierung) selbst zu einem Tupel wird.
Beispiel:
eval(fib(10), pow(10,2));
Es wird ein aktives Tupel erzeugt, das zunächst fib(10), sowie 10² berechnet, anschließend befindet
sich im Tupelraum das Tupel (55,100).
Linda: Beispiele
Um zu zeigen was man mit diesem Konzept nun anstellen kann, werden nun mehrere Beispiele
folgen. Als erstes, einfaches Bespiel werden wir die Datenstruktur n-Vector bauen, und zwar threadsave, d.h. mehrere Prozesse können die Datenstruktur gleichzeitig benutzen.
Konstruktor:
Vector(String name, int n){
for (int i=0;i<n;i++){
out(name, i, 0);
}
}
//Die Null ist ein willkürlich gewählter Startwert
Methoden:
get(String name, int i){
int value;
rd(name, i, ? value);
return value;
}
set(String name, int i, int value){
int old;
in(name, i, ? old);
out(name, i, value);
}
Auf diese Vektoren können nun mehrere Prozesse zugreifen, ohne dass es zu Schwierigkeiten
kommt. Dieses Beispiel implementiert nur Vektoren, die Ganzzahlen speichern, es sollte jedoch
nich schwierig sein dies auf beliebige Datentypen zu erweitern. Der große Vorteil gegenüber
klassischem message-passing ist, dass beliebig viele Prozesse auf diese Datenstruktur zugreifen
können, ohne das wir zu Beginn festlegen müssen, wie viele das genau sein werden. Es ist also in
Linda sehr elegant möglich 1: n Kommunikationen zu implementieren.
Das kann jedoch auch von Nachteil sein, da dadurch sehr sorgsames Programmieren gefordert ist.
Man schaue sich folgendes Beispiel an:
Prozess P1:
Vector("server",5);
//do something else
Prozess P2
int value;
in("server",3,? Value);
//do something else
Prozess P1 erzeugt den Vektor "server" um mit anderen Prozessen zu kommunizieren. Prozess P2,
welcher gar nichts mit P1 zu tun hat, wartet(möglicherweise um mit einem dritten Prozess zu
kommunizieren) auf das Tupel ("server",3,value) und zerstört damit die von P1 erzeugte
3.Vektorkomponente, was zu schwerwiegenden Fehlern führen kann. Es ist also nicht ohne weiteres
möglich innerhalb des Tupelraums zu abstrahieren. Der Programmierer muss genau wissen, wie die
Datenstruktur Vektor implementiert ist, um Fehler wie den obigen zu vermeiden.
Wenden wir uns nun einem klassischen Problem zu, dem Problem der fünf Philosophen. Dieses
wurde oft als eine Art Benchmark für die Ausdrucksstärke einer Programmiersprache bezüglich
Nichtsequenzialität betrachtet. Je einfacher es also zu lösen ist, desto größer ist seine
Ausdrucksstärke. Zunächst einmal eine Beschreibung des Problems:
Fünf Philosophen denken nach, bekommen danach Hunger und gehen zum Essen in einen
gemeinsamen Raum und sitzen an einem runden Tisch. Nach dem Essen denken sie wieder, usw...
Jeder Philosoph hat im Essraum ein Stäbchen links und ein Stäbchen rechts von sich zu liegen. Ein
Philosoph benötigt jedoch beide Stäbchen zum Essen.
Sind also alle gleichzeitig im Raum und jeder nimmt sich sein linkes Stäbchen, so entsteht ein
Deadlock.
Lösung in Linda:
initialize( ){
for (int i = 0; i < 5; i++) {
out("chopstick", i);
eval( Phil(i) );
if (i < 4) out("ticket");
}
}
Phil(int i){
while(l) C
think( ) ;
in("ticket") ;
in("chopstick" , i) ;
in("chopstick", (i+l) mod 5) ;
eat( );
out("chopstick", i);
out("chopstick", (i+i)%Num);
out("ticket");
}
//Wir erzeugen die Stäbchen
//Wir erzeugen die Philosophen
//Wir lassen max. 4 Philosophen in den Raum
}
Dadurch das maximal 4 Philosophen an den Tisch gelassen werden, wird ein Deadlock vermieden.
Die Stäbchen werden als Tupel simuliert, welche die Philosophen in Besitz nehmen können. Diese
Lösung ist einfach und leicht zu verstehen, jedoch nicht fair, da ein langsamer Philosoph, der auf
ein Ticket wartet, von einem anderen Philosophen, der sein Ticket frei gibt und anschließend direkt
wieder nimmt, unendlich oft überholt werden kann.
Für dieses Problem eine faire Lösung zu entwickeln ist überraschend aufwendig und schwierig.
Versucht man Scheduler-Eigenschaften herzustellen, so verliert Linda den Großteil seiner Eleganz.
Möchte man einfache Lösungen, so ist man dem Scheduling des Tupelraums ausgeliefert, über
welches in keiner meiner Quellen Aussagen gemacht werden, und welches sicherlich auch von der
jeweiligen Implementierung abhängt.
Ein weiteres Beispiel aus der Praxis, die Speicherverwaltung: Ein Verwalter soll eine bestimmte
Menge an Speicher verwalten. Prozesse können nun Speicher anfragen und wieder freigeben. Fragt
ein Prozess mehr Speicher an als vorhanden, so muss er solange warten, bis genügend Speicher da
ist. Lösung in Linda:
MemoryManager(int available){
out("Memory", available)
//Unser verfügbarer Speicher ist in available gespeichert
}
release(int amount){
int currentMemory;
in("Memory, ? currentMemory);
out("Memory", currentMemory+amount); //freigegebener Speicher wird hinzugefügt
}
request(int amount)
int currentMemory;
boolean done=false;
while(not done){
rd("Memory",? currentMemory)
//is there enough memory?
if (currentMemory >= amount){
in("Memory", ?currentMemory);
if (currentMemory >= amount){
//is there still enough memory?
out("Memory", currentMemory-amount);
done=true;
}
else{
out("Memory",currentMemory);
}
}
}
Der Konstuktor, sowie release(amount) verstehen sich von selbst. Der verfügbare Speicher wird in
dem Tupel ("Memory", available) gespeichert. Bei request(amount) wird zunächst der verfügbare
Speicher gelesen. Wird nicht mehr Speicher angefordert als vorhanden ist, so wird das Tupel
hereingeholt und nochmal überprüft ob genug Speicher vorhanden ist. Dies ist wichtig, da sich dies
seit dem Lesen verändert haben könnte. Erst nachdem wir erneut überprüft haben(diesmal mit in(),
um wechselseitigen Ausschluss zu garantieren), ob genug Speicher für uns vorhanden ist können
wir uns diesen nehmen und das Tupel aktualisieren.
Wozu war also die ursprüngliche Abfrage in der wir nur gelesen haben? Würden wir diese
weglassen, so könnte es passieren, dass ein schneller Prozess, der mehr Speicher braucht als
vorhanden ist, ständig die anderen Prozesse überholen würde. Das heißt er entfernt das Tupel mit
in() und sieht anschließend, dass nicht genügend Speicher vorhanden ist, sodass er das Tupel wieder
unverändert in den Tupelraum bringt, nur um direkt wieder von vorne zu beginnen. Durch die
Abfrage mit rd() wird es Prozessen, die mehr benötigen als vorhanden jedoch gar nicht erst erlaubt
das Tupel mit in() zu entfernen, wodurch Fortschritt garantiert ist.
Linda: Vorteile
-sehr einfaches und gut verständliches Konzept
-als Konstrukt unabhängig von Programmiersprachen, kann also jede Sprache ergänzen
-löst durchaus wichtige, nicht-triviale Probleme sehr elegant(z.B. Datenstruktur Vektor,
Speicherverwaltung, etc...)
-das Konzept des Tupelraums als einzige Möglichkeit zur Kommunikation und seine klaren
Schnittstellen ermöglicht es, dass mehrere Prozesse auf verschiedenen Rechnern(durchaus auch in
verschiedenen Programmiersprachen) problemlos zusammenarbeiten können.
Linda: Nachteile
-Abstraktion nicht ohne weiteres möglich(siehe Vektor Beispiel)
-Fairness herzustellen ist aufwendig
-Bei einem großen verteilten System ist die Idee sämtliche Kommunikation über einen einzigen
Tupelraum abzuwickeln zumindest fragwürdig, da die Suche von Tupeln mitunter lange dauern
kann, bzw. stark von der Implementierung des Tupelraums abhängt.
-hohe Fehleranfälligkeit: bereits ein Schreibfehler in einem in() beispielsweise "tickett" statt "ticket"
kann einen ganzen Prozess lahmlegen, ohne dass dieser Schreibfehler von einem Compiler erkannt
werden kann. Bei größeren Projekten dürfte dies die Fehlersuche erheblich erschweren.
Linda: Ausblick
- Erweiterung des Konzepts möglich, sodass mehrere Tupelräume unterstützt werden. Dies
ermöglicht unter anderem die Simulation von Channeln, wie sie in Occam vorkommen.
-Eine erweiterte Version von Linda wurde in java implementiert(Stichwort: javaSpaces). Unter
anderem werden die Befehle durch ein Argument erweitert, welches eine Zeit angibt, sodass
spezifiziert werden kann, wie lange ein Tupel sich im Tupelraum befinden soll, bzw. wie lange nach
einem Tupel gesucht werden soll.
Linda: persönliches Fazit
Die Idee Linda als reine Koordinationssprache zu benutzen ist sehr interessant und für kleinere
Projekte meiner Meinung nach auch durchaus sinnvoll. Dennoch ist äußerste Vorsicht beim
Programmieren geboten, da die Möglichkeit zur Abstraktion fehlt und Schreibfehler ganze Prozesse
lahmlegen können. Für größere Projekte würde ich daher von Linda in seiner ursprünglichen Form
eher abraten.
Quellen
N. Carriero, D. Gelernter: Linda in Context. CACM 32.4, April 1989:
http://dl.acm.org/citation.cfm?doid=63334.63337 (12.10.2011, 18:45)
D. Gelernter, N. Carriero: Coordination Languages and Their Significance. CACM 35.2, February
1992 :
http://dl.acm.org/citation.cfm?doid=129630.376083 (12.10.2011, 18:45)
JavaSpaces:
http://books.google.de/books?
hl=de&lr=&id=iq6uqLNkbwEC&oi=fnd&pg=PR15&dq=JavaSpaces&ots=TzyV3d0
haT&sig=ipt1HzwBdFk4pjM0pVAqWynIwNU#v=onepage&q&f=false (15.11.2011,
21:30)