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)