You are on page 1of 65

Skript zur Vorlesung Nebenl auge und verteilte Programmierung

WS 2012/2013

Priv. -Doz. Dr. Frank Huch 29. Januar 2013


Version: 1.1

Dieses Skript basiert auf einer studentischen Vorlesungsmitschrift von Lutz Semann, welche er dem Dozenten dankenswerter Weise zur Verf ugung gestellt hat.

Inhaltsverzeichnis
1 Einleitung 1.1 Sequentielle Programmierung . . . . . . . . . 1.2 Begrie . . . . . . . . . . . . . . . . . . . . . 1.2.1 Nebenl augkeit (Concurrency) . . . . 1.2.2 Parallelit at . . . . . . . . . . . . . . . 1.2.3 Verteilte Programmierung (distributed 1.3 Inhalt der Vorlesung . . . . . . . . . . . . . . 2 Nebenl auge Systeme 2.1 Kommunikation . . . . . . . . . . . . . . . . . 2.1.1 Semaphor (Dijkstra 68) . . . . . . . . 2.1.2 Producer-/Consumer Problem (andere 2.1.3 Dinierende Philosophen . . . . . . . . 2.1.4 Monitore (Dijkstra 71, Hoare 74) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Programming) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 1 1 1 1 2 2 2 3 4 4 5 6 7 7 8 8 8 8 9 10 11 11 12 13 14 17 17 18 22 22 23 24 24 24 26 27 28 29 29 30 30 30 30 32 32 34 34 34 35 36 36 40

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Verwendung von Semaphoren) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

3 Nebenl auge Programmierung in Java 3.1 Java . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.2 Interface der Thread-Objekte . . . . . . . . . . . . . 3.2.1 D amonthreads . . . . . . . . . . . . . . . . . 3.2.2 sleep()-Methode: . . . . . . . . . . . . . . . . 3.3 Synchronisation . . . . . . . . . . . . . . . . . . . . . 3.4 Synchronized Klassen-Methoden . . . . . . . . . . . 3.5 Synchronized Anweisungen . . . . . . . . . . . . . . 3.6 Unterscheidung der Synchronisation im OO-Kontext 3.7 Kommunikation zwischen Threads . . . . . . . . . . 3.7.1 Semantik . . . . . . . . . . . . . . . . . . . . 3.7.2 Variante von wait() mit Timeout: . . . . . . 3.7.3 Fallstudie: einelementiger Puer . . . . . . . 3.8 Beenden von Threadausf uhrungen . . . . . . . . . . 3.9 Warten auf Ergebnisse . . . . . . . . . . . . . . . . . 3.10 ThreadGroups . . . . . . . . . . . . . . . . . . . . . 4 Nebenl auge Programmierung 4.1 Concurrent Haskell . . . . 4.2 Kommunikation . . . . . . 4.3 Kan ale in Haskell . . . . .

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

in Haskell . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

5 Erlang 5.1 Sequentielle Programmierung in Erlang . . . 5.2 Nebenl auge Programmierung . . . . . . . . . 5.3 Ein einfacher Key-Value-Store . . . . . . . . . 5.4 Wie k onnen Prozesse in Erlang synchronisiert 5.5 MVar . . . . . . . . . . . . . . . . . . . . . . 5.6 Nebenl auge Programmierung in Erlang . . . 5.7 Verteilte Programmierung in Erlang . . . . . 5.8 Verbinden unabh angiger Erlang-Prozesse . . . 5.8.1 Ver anderungen des Clients . . . . . . . 5.8.2 Ver anderungen des Servers . . . . . . 5.9 Robuste Programmierung . . . . . . . . . . . 5.10 Systemabstraktionen . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . werden? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

6 Grundlagen der Verteilte Programmierung 6.1 7 Schichten des ISO-OSI-Referenzmodells . . . . 6.2 Protokolle des Internets . . . . . . . . . . . . . . 6.3 Darstellung von IP-Adressen/Hostnames in Java 6.4 Netzwerkkommunikation . . . . . . . . . . . . . . 6.4.1 UDP in Java . . . . . . . . . . . . . . . . 6.5 Socket-Optionen . . . . . . . . . . . . . . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

6.6 6.7

Sockets in Erlang . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Verteilung von Kommunikationsabstraktionen . . . . . . . . . . . . . . . . . . . . . . .

40 41 42 44 44 45 46 48 52 52 54 54 55 55 56 57 58 59 60 60

7 Synchronisation durch Tupelr aume 7.1 Java-Spaces . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7.2 Implementierung von Tupelr aumen in Erlang . . . . . . . . . . . . . . . . . . . . . . . 8 Spezikation und Testen von Systemeigenschaften 9 Linear Time Logic (LTL) 9.1 Implementierung von LTL zum Testen . . . . . . . . . . . . . . . . . . . . . . . . . . . 9.2 Verikation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9.3 Simulation einer Turing-Maschine . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 Transaktionsbasierte Kommunikation 10.1 Ein Bankbeispiel in Concurrent Haskell . . . . . . . 10.1.1 Gefahren bei der Programmierung mit Locks 10.2 Transaktionsbasierte Kommunikation (in Concurrent 10.2.1 Beispielprogramm . . . . . . . . . . . . . . . 10.3 Synchronisation mit Transaktionen . . . . . . . . . . 10.4 MVars . . . . . . . . . . . . . . . . . . . . . . . . . . 10.5 STM-Chan . . . . . . . . . . . . . . . . . . . . . . . 10.6 Alternative Komposition . . . . . . . . . . . . . . . . 10.7 Implementierung von STM . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Haskell als anderen Ansatz) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

ii

1 Einleitung
1.1 Sequentielle Programmierung
In der sequentiellen Programmierung f uhren Programme Berechnungen Schritt f ur Schritt aus. Hierdurch ergeben sich insbesondere Probleme bei der Entwicklung reaktiver Systeme, wie z.B. GUIs, Betriebssystemprogrammen oder verteilten/Web-Applikationen. Hier sollen alle m oglichen Anfragen/Eingaben quasi gleichzeitig behandelt werden k onnen. Ein erster Ansatz hierzu war Polling. Alle m oglichen Anfragen werden in einer groen Schleife bearbeitet. Ein Reaktives System l auft potenziell unendlich lange und reagiert auf seine Umgebung (z. B. GUI, Textverarbeitung, Webserver, Betriebssystemprozesse. Eine Besonderheit ist hierbei dass meist mehrere Anfragen/Eingaben m oglichst gleichzeitig bearbeitet werden sollen. Wie ist es m oglich Reaktivit at zu gew ahrleisten? Ein erster Ansatz w are mittels Polling m oglich, d. h. der Abfrage aller m oglichen Anfragen in einer groen Schleife, welcher jedoch folgende Nachteile hat: Busy Waiting verbraucht System Ressourcen Code ist schlecht strukturiert, da alle Alle Anfragen in die Schleife integriert werden m ussen. Modularit at ist nur eingeschr ankt u oglich. ber Unterprozeduren m Bearbeitung einer Anfrage blockiert andere Anfragen, da R uckkehr in Schleife erst nach Beendigung vorheriger Anfragen. Somit sequentielles Bearbeiten der Anfragen und keine Reaktivit at w ahrend der Bearbeitung einer Anfrage. L osung:Nebenl augkeit (Concurrency ) Mehrere Threads (F aden) k onnen nebenl aug ausgef uhrt werden und die einzelnen Dienste eines reaktiven Systems zur Verf ugung stellen. Hierdurch ergeben sich folgende Vorteile: kein Busy Waiting (auf Interruptebene verschoben) Bessere Strukturierung: Dienst = (ggf. mehreren) Threads eventuell k onnen mehrere Anfragen gleichzeitig bearbeitet werden System ist immer reaktiv Beachte hierbei, dass die letzten beiden Punkte von der Implementierung der Threads auf (Betriebs)Systemebene abh angen.

1.2 Begrie
1.2.1 Nebenl augkeit (Concurrency) Durch die Verwendung von Threads/Prozessen k onnen einzelne Aufgaben/Dienste eines Programms unabh angig von anderen Aufgaben/Diensten programmiert werden. 1.2.2 Parallelit at Meist in Verbindung mit High-Performance-Computing. Durch die parallele Ausf uhrung mehrerer Prozesse (auf in der Regel mehreren Prozessoren) wird eine schnellere Ausf uhrung angestrebt. Beachte aber, dass manchmal auch auf Ein-Prozessor-Systemen durch Nebenl augkeit Performance-Gewinne erreicht werden k onnen, z.B. bei geringerer Performance von Netzwerkkarten. Fr uher wurden insbesondere Shared-Memory-Systeme betrachtet, welche heute aber wieder in Form der Multicore-Processoren an Relevanz gewinnen. Allerdings werden oft auch Rechner-Cluster zur parallelen Programmierung eingesetzt (Dristributed-Memory-Systeme) bis hin zu Netzwerken im Internet. Der Vorteil ist die Unabh angigkeit von Spezialhardware, sowie eine einfache Steigerung der zur Verf ugung stehenden Prozessoren. Das gr ote Problem bei der parallelen Programmierung ist in der Regel eine m oglichst gleichm aige Ausnutzung der Prozessoren (Vermeidung von Sequentialisierung) und eine Minimierung der Kommunikation.

Wir werden in dieser Vorlesung nicht weiter speziell auf die paralelle Programmierung eingehen. Die pr asentierten Techniken nden hierbei aber durchaus auch Anwendung, wenngleich der Fokus sicherlich meist auf andere Aspekte gerichtet werden muss. 1.2.3 Verteilte Programmierung (distributed Programming) Ein verteiltes System besteht aus mehreren physisch getrennten Prozessoren, welche u ber ein Netzwerk verbunden sind. Zum einen werden solche Systeme wie zuvor erw ahnt zur parallelen Programmierung eingesetzt. Dar uber hinaus sind aber viele Anwendungen von ihrer Aufgabenstellung schon verteilt, so dass sie auch entsprechend implementiert werden m ussen. Beispiele sind Telefonsysteme oder Chats. Manchmal stehen auch bestimmte Ressourcen nur an bestimmten Stellen zur Verf ugung, so dass ebenfalls eine Verteilung der Anwendung notwendig ist, z.B. Geldautomaten, spezielle Datenserver. Eine Verteilung wird manchmal auch zur Performancesteigerung, z.B. Lastverteilung eingesetzt und kann auch zur Fehlertoleranz durch redundante Informationsvorhaltung eingesetzt werden. Verteilte Systeme werden in der Regel mit lokaler Nebenl augkeit kombiniert, um Reaktivit at gegen uber der Netzwerkkommunikation zu erm oglichen. Durch mehrere verteilte Prozesse kann es aber auch ohne Nebenl augkeit zu den gleichen Problemen wie bei rein nebenl augen Systemen kommen.

1.3 Inhalt der Vorlesung


Vergleich unterschiedlicher Konzepte zur nebenl augen und verteilten Programmierung am Beispiel von Java, Erlang, Concurrent Haskell und weiteren Konzepten. Ausgew ahlte verteilte Algorithmen.

2 Nebenl auge Systeme


Generell sind immer drei Aspekte zu ber ucksichtigen: Denition und Start von Threads (ggf. auch deren Terminierung) Kommunikation zwischen Threads Synchronisation von Threads Hierbei h angen die beiden letzten Punkte stark miteinander zusammen, da Kommunikation nur schwerlich ohne Synchonisation m oglich ist und jede Synchronisation nat urlich auch eine Art Kommunikation ist. Wir betrachten folgendes Beispiel in Pseudocode:
1 2

par { while true do w r i t e ( a ) } { while true do w r i t e ( b ) } , wobei Semantik des par-Konstrukts: Spaltet Thread in zwei Unterthreads auf und f uhrt diese nebenl aug aus. ababab ... aaabaaabaabbbaa ... aaaaa ... bbbbbb ... uhrung d. h. gleiche Ausgabe bei allen L aufen), die durch Oft (aber nicht immer) deterministische Ausf den Scheduler geregelt wird. Dies muss aber nicht immer der Fall sein und ein Programmierer darf dich hierauf auf keinen Fall verlassen. Der Schedule teilt den Prozessen die Prozessorzeit zu. Im Wesentlichen werden dabei folgende Arten von Schedulern unterschieden: kooperatives Multitasking ein Thread rechnet solange, bis er die Kontrolle abgibt (z. B. mittels yield). Die Anderen warten (suspendiert) auf das Ergebnis (z. B. aaaaa ... und bbbbbb ...) pr aemptives Multitasking der Scheduler entzieht den Prozessen regelm aig die Kontrolle. Dies erfordert eine aufw andigere Implementierung, bietet aber mehr Programmierkomfort zur

Gew ahrleistung der Reaktivit at des Systems (z. B. alle Ausgaben auer aaaaa ... und bbbbbb ...) Prinzipiell sollten Programmierer nebenl auger Systeme m oglichst f ur beliebige Scheduler entwickeln. Einzige Ausnahme stellt manchmal die Einschr ankung auf p aemptives Multitasking dar. Aber selbst dies ist oft nicht notwendig, da durch Synchronisation die Kontrolle h aug abgegeben wird und somit auch kooperatives Multitasking ausreicht.

2.1 Kommunikation
ird oft u ber geteilte Variablen realisiert. Listing 1: Beispiel
1 2 3

int i = 0; par { i := i + 1 } { i := i 2 } write ( i ) ; Semantik: wie zuvor Es Ergibt sich Nichtdeterminismus mit mehreren Ergebnissen: i = 0; i = i + 1; i = i * 2 i = 0; i = i * 2; i = i + 1 mit dem Ergebnis 2 mit dem Ergebnis 1

Wann ist ein Programm also korrekt? ein sequentielles Programm ist korrekt, falls es f ur alle Eingaben ein korrektes Ergebnis liefert. ein nebenl auges Programm ist korrekt, wenn es f ur alle Eingaben und alle Schedules das richtige Ergebnis liefert. In der Praxis (leider) oft f ur einen (den testbaren) Schedule, wobei folgende Probleme auftreten k onnen: Portierung auf andere Architektur anderer Schedule Erweiterung des Systems um zus atzliche Threads/zus atzliche Berechnungen ver andertes Scheduling Beeinussung durch Ein-/Ausgaben Deshalb: f ur alle Schedules !!! (manchmal ist auch eine Einschr ankung auf pr aemptives Multitasking okay) Bei Korrektheit f ur alle Schedules verliert man den Vorteil des pr aemptiven Multitaskings. Deshalb wird die Korrektheit oft nur f ur alle fairen Schedules verlangt. Der Begri fair bedeutet hierbei, dass jeder Thread nach endlicher Zeit wieder an die Reihe kommt, also falls m oglich einen Schritt ausf uhrt. Als Konsequenz daraus ist das Testen der Programme schwierig, da ggf. unendlich viele Abl aufe betrachtet werden m ussen und eine Beeinussung des Schedules fast unm oglich ist. Deshalb: formale Spezikation, formale Verikation, Testtools und Debugger mit Einuss auf den Scheduler. Weiteres Problem im Beispiel: Welche Anweisungen sind atomar? Der Compiler wird ein solches PRogramm sicherlich in Maschinencode oder Pseudocode f ur eine abstrakte Maschine u bersetzen. Listing 2: Stackmaschinencode des Programms int i = 0; par { LOAD i ; LIT 1 ; ADD; STORE i } { LOAD i ; LIT 2 ; MULT; STORE i } Aufgrund dieses Codeaufbaus ist dann ebenfalls folgende Ausf uhrung m oglich: LOAD i; LIT 2; MULT; LOAD i; LIT 1; ADD; STORE i; STORE i

1 2 3

Diese liefert das Ergebnis i = 0, was sicherlich nicht das gew unschte Ergebnis ist. Bei dieser Ausf uhrung ist der 1. Thread in den 2. Thread eingebettet. Zuweisungen k onnen aber nicht generell atomar gemacht werden (wegen z. B. groer Berechnungen, Funktionsaufrufe) L osung: Der Programmierer bekommt die M oglichkeit zur Synchronisation. Im Folgenden werden zun achst einige bekannte Konzepte wiederholt.

2.1.1 Semaphor (Dijkstra 68) Ziel: atomare (ununterbrochene) Ausf uhrung von Programmabschnitten. Semaphore besitzen eine feste Schnittstelle, bestehen aus einem Integer und einer zugeordneten Prozesswarteschlange und stellen einen abstrakten Datentyp mit zwei atomaren Operationen P und V dar, wobei die Operationen der Ver anderung der Variablen dienen Listing 3: Semaphor-Operationen
1 2 3 4 5 6 7 8 9 10

P( s ) : IF ( s >= 1 ) THEN { s := s 1 ; } ELSE { P r o z e s s wird i n Warteschlange zu s e i n g e t r a g e n und s u s p e n d i e r t ; s := s 1 ; } V( s ) : s := s + 1 ; IF ( Warteschlange zu s n i c h t l e e r ) THEN { Erwecke e r s t e n P r o z e s s d e r Warteschlange zu s ; } Das P steht hierbei f ur passieren (passeer) und das V f ur verlassen (verlaat). Das obige Beispiel l asst sich mittels Semaphor also wie folgt implementieren, was das Ergebnis i = 0 verhindert:

1 2 3

int i = 0; semaphor s := 1 ; par { P( s ) ; i := i + 1 ; V( s ) ; } { P( s ) ; i := i 2 ; V( s ) ; } Der initiale Wert des Semaphors bestimmt die maximale Anzahl der Prozesse im kritischen Bereich. Meist ist dies 1, was zu einem bin aren Semaphor f uhrt. 2.1.2 Producer-/Consumer Problem (andere Verwendung von Semaphoren) n Producer erzeugen Waren, die dann von m Consumern verbraucht werden. Zun achst gehen wir bei der Modellierung von der Verwendung eines Unbeschr ankten Puers aus: Listing 4: Zun achst mit unbeschr anktem Puer

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

semaphor num := 0 ; Producer : while ( true ) { produce p r o d u c t ; push p r o d u c t t o b u f f e r ; V(num ) ; } Consumer : while ( true ) { P(num ) ; p u l l p r o d u c t from b u f f e r ; consume p r o d u c t ; } Bemerkungen: Zeile 1: F ullstand des Puers Zeile 12: an dieser Stelle ndet ggf. eine Suspension statt Die Semaphore fungiert als Produktz ahler f ur den Puer. Hochz ahlen mittel V im Producer und Herunterz ahlen mittels P im Consumer. Wir haben aber zun achst noch keine Synchronisation auf dem Puer modelliert (kritischer Bereich). Um hier zus atzlich synchronisieren zu k onnen, f ugen wir einen zweiten Semaphor ein, welcher wie oben skiziert den kritischen Bereich sch utzt.

Listing 5: Zus atzlich mit Synchronisation


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

semaphor num := 0 ; semaphor b a c c e s s := 1 ; Producer : while ( true ) { produce p r o d u c t ; P( b a c c e s s ) ; push p r o d u c t t o b u f f e r ; V( b a c c e s s ) ; V(num ) ; } Consumer : while ( true ) { P(num ) ; P( b a c c e s s ) ; p u l l p r o d u c t from b u f f e r ; V( b a c c e s s ) ; consume p r o d u c t ; } Bemerkung: Zeile 1: F ullstand des Puers Zeile 15: an dieser Stelle ndet ggf. eine Suspension statt Es f allt auf, dass die verwendung von P-/V-Operationen schnell un ubersichtlich werden kann. Es gibt keine strukturelle Zuordnung von P/V. 2.1.3 Dinierende Philosophen An einem Tisch mit Essen sitzen 5 Philosophen. Es gibt allerdings nur 5 St abchen zum Essen, wobei eine Person immer 2 St abchen ben otigt, so dass jeweils 1 St abchen mit dem Nachbarn geteilt werden muss. Ein Philosoph denkt so lange, bis er hungrig wird und versucht dann nacheinander beide St abchen aufzunehmen und zu essen. Es ndet keine zus atzliche Kommunikation untereiander statt. Die Implementierung mit Semaphoren sieht wie folgt aus: Listing 6: Ein Philosoph i von n l asst sich dann wie folgt implementieren

1 2 3 4 5 6 7 8 9

while ( true ) { think ( ) ; g e t hungry ( ) ; P( s t i c k ( i ) ) ; P( s t i c k ( ( i +1)%n ) ) ; eat ( ) ; V( s t i c k ( i ) ) ; V( s t i c k ( ( i +1)%n ) ) ; } Die Reihenfolge der V -Operationen ist hierbei egal. Problem der Implementierung: Deadlocks (Verklemmungen), falls alle Philosophen ein St abchen nehmen. Eine m ogliche L osung wird durch ein Zur ucklegen des ersten St abchens aufgel ost, falls kein Zweites verf ugbar ist. Listing 7: Implementierung mit Zur ucklegen

1 2 3 4

while ( true ) { think ( ) ; g e t hungry ( ) ; P( s t i c k ( i ) ) ;

5 6 7 8 9 10 11

IF ( s t i c k ( ( i +1)%n ) == 0 ) THEN { V( s t i c k ( i ELSE { P( s t i c k ( ( eat ( ) ; V( s t i c k ( i V( s t i c k ( ( } }

)); } i +1)%n ) ) ; )); i +1)%n ) ) ;

An der Stelle P(stick((i+1)%n)) kann ein anderer Philosoph das St abchen wegnehmen. Der letzte Philosoph muss sein St abchen aber immer wieder weglegen. Livelocks sind aber nicht ausgeschlossen, d. h. ein oder mehrere Philosophen k onnen verhungern (trotz eines fairen Schedules). Nachteile von Semaphoren: P-/V-Operatoren sind schwer zuordbar (sind eigentlich wie Klammern, aber nur w ahrend der Laufzeit; nicht in der Syntax) Codestrukturierung ist aus softwaretechnischer Sicht schwer und bietet viele m ogliche Fehlerquellen lieber: strukturiertes Konzept 2.1.4 Monitore (Dijkstra 71, Hoare 74) Monitore entsprechen einer Menge von Prozeduren und Datenstrukturen, die als Betriebsmittel betrachtet mehreren Threads zug anglich sind, aber nur von einem Thread zur Zeit benutz werden k onnen in einem Monitor bendet sich h ochstens ein Thread. Als Besipiel betrachten wir die Monitor-Implementierung eines Puers: Listing 8: Implementierung eines beschr ankten Puers als Monitor
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21

monitor b u f f e r { [ i n t ] [ 0 . . . ( n 1)] c o n t e n t s ; i n t num , wp , rp = 0 ; queue s e n d e r , r e c e i v e r ; p r o c e d u r e push ( i n t item ) { IF (num == n ) THEN { d e l a y ( s e n d e r ) ; } c o n t e n t s [ wp ] = item ; wp := (wp+1) mod n ; num := num + 1 ; continue ( r e c e i v e r ) ; } function int pull () { IF (num == 0 ) THEN { d e l a y ( r e c e i v e r ) ; } i n t item := c o n t e n t [ rp ] ; rp := ( rp +1) mod n ; num := num 1 ; continue ( s e n d e r ) ; r e t u r n item ; } } Bemerkungen: Zeile 4: Prozessqueues Zeile 6: Puer ist voll Zeile 14: Puer ist leer Semantik: num als Anzahl der Elemente im Puer wp als Pointer zum Schreiben rp als Pointer zum Lesen delay(Q) der aufrufende Prozess wird in die Warteschlange Q eingef ugt continue(Q) aktiviert den ersten Thread in der Warteschlange Q, falls ein solcher vorhanden ist

Vorteile: innerhalb der Monitorprozesse k onnen Synchronisationsprobleme ignoriert werden alle kritischen Bereiche/Datenstrukturen benden sich in einem abstrakten Datentyp Nachteile: h aug zu viel Synchronisation sequentielles Programm oder Deadlocks wird in Programmiersprachen selten unterst utzt (in Java existiert ein monitor ahnliches Konzept)

3 Nebenl auge Programmierung in Java


3.1 Java
Programmiersprache des Internets Reaktivit at notwendig Nebenl augkeit. Klasse Thread im Paket java.lang: eigene Threads als Unterklasse von Thread implementierbar eigentlicher Code in run()-Methode Anlegen von Threads mit new und Konstruktor1 Ausf uhrung des Threads mittels der Methode start()2 Listing 9: Beispiel
1 2 3 4 5 6 7 8 9

p u b l i c c l a s s C o n c u r r e n t P r i n t e r e x t e n d s Thread { private String str ; public CurrentPrint ( String s t r 1 ) { s t r = s t r 1 ; } p u b l i c v o i d run ( ) { while ( true ) { System . out . print ( s t r + ) ; } } p u b l i c s t a t i c v o i d main ( S t r i n g [ ] a r g s ) { new C o n c u r r e n t P r i n t e r ( a ) . s t a r t ( ) ; new C o n c u r r e n t P r i n t e r ( b ) . s t a r t ( ) ; } } Ausgabe: Menge aus as und bs. Bei der Verwendung von green Threads, ggf. auch nur as oder nur bs. Wegen fehlender Mehrfachvererbung in Java ist oft das Erben von der Threadklasse problematisch. Deshalb besteht alternativ die M oglichkeit der Implementierung des Interfaces Runnable:

1 2 3 4 5 6 7 8 9 10 11

p u b l i c c l a s s C o n c u r r e n t P r i n t implements Runnable { String str ; public ConcurrentPrint ( String s t r 1 ) { s t r = s t r 1 ; } p u b l i c v o i d run ( ) { while ( true ) { System . out . print ( s t r + ) ; } } p u b l i c s t a t i c v o i d main ( S t r i n g [ ] a r g s ) { Runnable aThread = new C o n c u r r e n t P r i n t ( a ) ; Runnable bThread = new C o n c u r r e n t P r i n t ( b ) ; new Thread ( aThread ) . s t a r t ( ) ; new Thread ( bThread ) . s t a r t ( ) ; } }

Achtung this liefert jetzt kein Thread-Objekt mehr, sondern ein Runnable-Objekt. Das aktuelle Thread-Objekt bekommt man mittels Thread.currentThread().

1 initialisiert 2 diese

nur die Attribute ist also sehr klein! f uhrt die run()-Methode aus

3.2 Interface der Thread-Objekte


Name standard main Thread, Thread-0, Thread-1, ... / getName, setName Priorit aten Zahlen zwischen MIN_PRIORITY(1) und MAX_PRIORITY(10) mit Standard-Wert NORM_PRIORITY(5) / setPriority, getPriority ( auf die Priorit aten darf man sich aber nicht verlassen!) Threadgruppe Zuordnung von Threads zu Gruppen als Strukturierungskonzept Zust ande: erzeugt wird durch new Thread() erreicht aktivierbar wird durch start oder yield sowie dem Eingabeende oder dem Ablauf der Sleep-Zeit erreicht terminiert wird erreicht, wenn run beendet wird nicht aktivierbar wird durch das Warten auf eine Eingabe oder ein sleep erreicht Das Thread-Objekt bleibt erhalten, solange es referenziert wird. W ahrend der Ausf uhrung existiert eine Eigenreferenz Durch Aufruf der Methode yield wird dem Scheduler signalisiert, dass der Prozess die Kontrolle abgeben kann. Bei green Threads f uhrt dieser Aufruf in der Regel zum Threadswitch, falls dies m oglich ist. Bei pr aemptivem Scheduling kann yield auch keinen Efekt haben. 3.2.1 D amonthreads setDaemon(true) von start(), um einen Thread als D amonthread zu deklarieren. Idee: unwichtiger Hintergrundthread Java Virtual Machine (JVM) terminiert, falls nur noch D amonthreads existieren. Beispiel: AWT-Thread, Serverthreads, Garbage Collector Scheduler: Es gibt unterschiedliche Implementierungen green Threads kooperativer Scheduler in JVM (wird in der Regel nicht mehr unterst utzt) native Threads Verwendung von Betriebssystemprozessen mit pr aemptivem Multitasking Vorteil: Ausnutzung moderner Multicore-Architekturen Wiederverwendung des Betriebssystem-Schedulers Nachteil: gr oerer Verbrauch von Systemressourcen (keine light-weight Threads) 3.2.2 sleep()-Methode: Die Objekt-Methode sleep deaktiviert den ausf uhrenden Thread um die angegebene Zeit in Millisekunden. sleep kann eine InterruptedException werfen. Hierzu sp ater mehr. Listing 10: erste Variante
1 2

t r y { Thread . c u r r e n t T h r e a d ( ) . sleep ( 3 0 0 0 ) ; } catch ( Interrupted Exceptions e ) { . . . } Listing 11: zweite Variante

t h i s . sleep ( 3 0 0 0 ) ;

3.3 Synchronisation
Kombination aus Lock/Unlock-Konzept und Monitoren. Thread-Methoden k onnen als synchronized deklariert werden:

In allen synchronized-Methoden eines Objektes darf sich maximal ein Thread aufhalten. Hierzu z ahlen auch Unterbrechungen (auch unsynchronisierte Methoden), die in einer synchronizedMethode aufgerufen werden. kein Verlassen der synchronized-Methode durch sleep oder yield. andere Sicht jedes Objekt besitzt einen Lock beim Versuch der Ausf uhrung einer synchronized-Methode wird wie folgt vorgegangen: Lock verf ugbar Lock nehmen und fortfahren eigener Thread hat Lock fortfahren sonst auf Lock suspendieren Lock wird beim Verlassen der synchronized-Methode wieder freigegeben (auch bei Terminierung oder Exception) Im Vergleich zu Semaphoren sind synchronisierte Methoden: strukturorientiert ein unlock kann nicht vergessen werden keine Suspension bei mehrfachem Nehmen des Locks Im Vergleich zum Monitorkonzept sind synchronisierte Methoden exibler, da einzelne Methoden unsynchronisiert bleiben k onnen, aber auch gr oere Gefahr von Fehlern. Ein einfaches Beispiel soll die Verwendung von synchronized- Methoden veranschaulichen. Wir betrachten eine Implementierung einer Klasse f ur ein Bankkonto: Listing 12: Beispiel
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

c l a s s Account { p r i v a t e double balance ; p u b l i c Account ( d o u b l e i n i t i a l ) { b a l a n c e = i n i t i a l ; } p u b l i c synchronized double getBalance ( ) { return balance ; } p u b l i c s y n c h r o n i z e d v o i d d e p o s i t ( d o u b l e amount ) { b a l a n c e += amount ; } } Account a = new Account ( 3 0 0 ) ; ... a . deposit (100) . . . a . deposit (100); \ ldots Die m oglichen Ergebnisse ohne Synchronisation sind 400, 500 oder richtiger Unsinn (z. B. falls der Double teilweise geschrieben wird, da diese Operation nicht atomar ist). Mit Synchronisation ist nur das Ergebnis 500 m oglich. synchronized ist f ur Konstruktoren nicht erlaubt, aber auch nicht sinnvoll, da diese immer nur von einem Thread ausgef uhrt werden. Vererbte synchronized-Methoden m ussen nicht synchronisiert sein ( verfeinerte Implementierung). Nicht u berschriebene Methoden bleiben synchronized. Nicht-synchronisierte Methoden k onnen beim Uberschreiben synchronized werden. // n e b e n l a e u f i g e Ausfuehrung

3.4 Synchronized Klassen-Methoden


Auch Klassenmethoden k onnen synchronisiert werden: static synchronized Analog wie oben, nur auf Klassenebene. Keine Wechselwirkung mit synchronized Objektmethoden.

3.5 Synchronized Anweisungen


Vererbte synchronisierte Methoden m ussen nicht zwingend wieder synchronisiert sein. Wenn man solche Methoden u usselwort synchronized auch weglassen. Dies beberschreibt, so kann man das Schl zeichnet man als verfeinerte Implementierung. Die Methode der Oberklasse bleibt dabei synchronized. Andererseits k onnen unsynchronisierte Methoden auch durch synchronisierte u berschrieben werden. Klassenmethoden, die als synchronisiert deklariert werden (static synchronized) haben keine Wechselwirkung mit synchronisierten Objektmethoden. Die Klasse hat also ein eigenes Lock. Man kann sogar auf einzelne Anweisungen synchronisieren:
1

s y n c h r o n i z e d ( expr ) b l o c k Dabei muss expr zu einem Objekt auswerten, dessen Lock dann zur Synchronisation verwendet wird. Streng genommen sind synchronisierte Methoden also nur syntaktischer Zucker: So steht die Methodendeklaration

s y n c h r o n i z e d A m( a r g s ) b l o c k eigentlich f ur

A m( a r g s ) { s y n c h r o n i z e d ( t h i s ) b l o c k } Einzelne Anweisungen zu synchronisieren ist sinnvoll, um weniger Code synchronisieren bzw. sequentialisieren zu m ussen:

1 2 3 4 5 6 7 8 9 10 11 12

p r i v a t e double s t a t e ; p u b l i c void c a l c ( ) { double r e s ; // do some r e a l l y e x p e n s i v e c o m p u t a t i o n ... // s a v e t h e r e s u l t t o an i n s t a n c e v a r i a b l e synchronized ( t h i s ) { state = res ; } } Synchronisierung auf einzelne Anweisungen ist auch n utzlich, um auf andere Objekte zu synchronisienen. Wir betrachten als Beispiel eine einfache Implementierung einer synchronisierten Collection:

1 2 3 4 5 6 7 8 9 10 11

c l a s s Store { p u b l i c s y n c h r o n i z e d b o o l e a n hasSpace ( ) { ... } public synchronized void i n s e r t ( i n t i ) throws N o S p a c e A v a i l a b l e E x c e p t i o n { ... } } Wir m ochten diese Collection nun verwenden wie folgt:

1 2 3 4 5

S t o r e s = new S t o r e ( ) ; ... i f ( s . hasSpace ( ) ) {

10

6 7

s . insert (42); } Dies f uhrt jedoch zu Problemen, da wir nicht ausschlieen k onnen, dass zwischen den Aufrufen von hasSpace() und insert(int) ein Re-Schedule geschieht. Da sich das denieren spezieller Methoden f ur solche F alle oft als unpraktikabel herausstellt, verwenden wir die obige Collection also besser folgendermaen:

1 2 3 4 5

synchronized ( s ) { i f ( s . hasSpace ( ) ) { s . insert (42); } }

3.6 Unterscheidung der Synchronisation im OO-Kontext


Wir bezeichnen synchronisierte Methoden und synchronisierte Anweisungen in Objektmethoden als server-side synchronisation . Synchronisation der Aufrufe eines Objektes bezeichnen wir als client-side synchronisation . Aus Ezenzgr unden werden Objekte der Java-API, insbesondere Collections, nicht mehr synchronisiert. F ur Collections stehen aber synchronisierte Versionen u ber Wrapper wie synchronizedCollection, synchronizedSet, synchronizedSortedSet, synchronizedList, synchronizedMap oder synchronizedSortedMap zur Verf ugung. Sicheres Kopieren einer Liste in ein Array kann nun also auf zwei verschiedene Weisen bewerkstelligt werden: Als erstes legen wir eine Instanz einer synchronisierten Liste an:
1 2 3 4 5 6

L i s t < I n t e g e r > u n s y n c L i s t = new L i s t < I n t e g e r > ( ) ; // f i l l t h e l i s t ... L i s t <I n t e g e r > l i s t = C o l l e c t i o n s . s y n c h r o n i z e d L i s t ( u n s y n c L i s t ) ; Nun k onnen wir diese Liste entweder mit der einfachen Zeile

I n t e g e r [ ] a = l i s t . toArray ( new I n t e g e r [ 0 ] ) ; oder u ber

1 2 3 4 5 6

Integer [ ] b ; synchronized ( l i s t ) { b = new I n t e g e r [ l i s t . s i z e ( ) ] ; l i s t . toArray ( b ) ; } in ein Array kopieren. Bei der zweiten, zweizeiligen Variante ist die Synchronisierung auf die Liste unabdingbar: Wir greifen in beiden Zeilen auf die Collection zu, und wir k onnen nicht garantieren, dass nicht ein anderer Thread die Collection zwischenzeitig ver andert. Dies ist ein klassisches Beispiel f ur Client-side synchronisation.

3.7 Kommunikation zwischen Threads


Komunikation geschieht u alt die Variable den Wert? Als ber geteilte Objekte Problem: Wann erh Beispiel betrachten wir den einfachen Fall, dass ein Thread einem anderen einen Wert mitteilt und dieser beim Empf anger ausgegeben wird. Den Code k onnen wir in folgender Klasse zusammenfassen:
1 2

class C { private int state = 0;

11

3 4 5 6 7 8 9 10 11 12 13 14

private boolean modified = false ; p u b l i c synchronized void printNewState ( ) { while ( ! m o d i f i e d ) { } ; System . out . p r i n t l n ( s t a t e ) ; modified = false ; } public synchronized void setValue ( i n t v ) { state = v ; m o d i f i e d = true ; System . out . p r i n t l n ( v a l u e s e t ) ; } }

Diese erste L osung zeigt den erfolgten Versand durch Ver anderung eines geteilten Objekts, hier boolesches Flag modified an. Ein groer Nachteil dieser L osung ist sicherlich das Busy Waiting beim Leser (Wurde modified ver andert?). Als Verbesserung k onnen Threads in Java mittels wait()3 und Aufwecken mittels notify(). Es ergibt sich folgender Code:
1 2 3 4 5 6 7 8 9 10 11 12

class C { private int state = 0; p u b l i c synchronized void printNewState ( ) { wait ( ) ; System . out . p r i n t l n ( s t a t e ) ; } public synchronized void setValue ( i n t v ) { state = v ; notify (); System . out . p r i n t l n ( v a l u e s e t ) ; } }

Thread 1 f uhrt printNewState() und Thread 2 f uhrt setValue(42) nebenl aug aus. Dies ergibt die Ausgabe value set, 42 und nicht 42, value set und auch nicht 0! Bevor wir das Beispiel weiter verfeinern, wollen wir zun achst die Semantik der Methoden wait() und notify() n aher beleuchten. 3.7.1 Semantik wait() suspendiert den ausf uhrenden Thread und gibt den Lock des Objektes frei notif y () erweckt einen4 Thead des Objektes und f ahrt direkt mit der eigenen Berechnung fort. Wenn kein Thread suspendiert ist, dann wird nur fortgefahren ( ein notify() wird nicht gespeichert) Ausgabe von Thread 1 nach der Ausgabe von Thread 2 oder Thread 2 ist fertig und anschlieen des Suspendieren von Thread 1 (hier keine Ausgabe). printNewState() kann nur Anderungen ausgeben, die nach seinem wait()-Aufruf passieren. Wie k onnen wir gew ahrleisten, dass auch vorhergehende Nachrichten ausgeben werden? Wie bei der busy waiting-Idee verwenden wir zus atzlich noch ein Flag:
1 2 3 4 5 6 7

private boolean modified = false ; p u b l i c synchronized void printNewState ( ) { i f ( ! modified ) { wait ( ) ; } System . out . p r i n t l n ( s t a t e ) ; modified = false ; }

3 verl asst 4 es

die Methode und versucht beim Aufwecken die synchronized Methode wieder neu zu betreten ist nicht genau festgeltegt welchen!

12

In setValue() wird zus atzlich (irgendwo) modified = true; eingef ugt. Somit gehen Nachrichten, welche fr uher gesendet wurden nicht verloren. Was passiert aber bei mehreren schreibenden Threads? Die erste Zustands anderung kann u osung, m ussen bersehen werden und nicht ausgegeben werden. Als L wir also auch die Sender synchronisieren. Diese m ussen also warten, bis ein alter WErt verarbeitet wurde. Listing 13: Nichtfunktionierende L osung
1 2 3 4 5 6 7

public synchronized void setValue ( i n t v ) { i f ( modified ) { wait ( ) ; } state = v ; notify (); m o d i f i e d = true ; System . out . p r i n t l n ( v a l u e s e t ) ; }

Nun stellt sich aber das Problem, dass notify() anstelle eines Lesers einen Schreiber erweckt und der erste geschriebene Wert immer noch verloren geht. Es ist also notwendig, alle schlafenden Threads zu erwecken einen lesenden Thread erweckt? M oglicherweise wird ein weiterer Schreiber erweckt und die Nachricht geht immer noch verloren. Eine m ogliche L osung ist alle schlafenden Threads zu erwecken und u alschlicherweise erweckten Threads wieder schlafen legen: ber ein bisschen Busy Waiting, die f
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17

class C { private int state = 0; private modified = false ; p u b l i c synchronized void printNewState ( ) { while ( ! m o d i f i e d ) { w a i t ( ) } ; modified = false ; notify (); System . out . p r i n t l n ( s t a t e ) ; } public synchronized void setValue ( i n t v ) { while ( m o d i f i e d ) { w a i t ( ) } ; state = v ; m o d i f i e d = true ; notify (); System . out . p r i n t l n ( v a l u e s e t ) ; } }

Es werden also alle suspendierten Objkte aufgeweckt und das richtige erkennt sich selber. Dies ist der Java-Stil zur synchronisierten Kommunikation: Verwendung von wait() in Kombination notifyAll) und einen bisschen Busy Waiting: while(!Nachricht erhalten) { wait(); } und notifyAll(); Dies hat aber folgende Nachteile: sehr unkontrollierte Kommunikation Verschwendung von Systemressourcen (Busy Waiting) selbst bei passender wait-/notify()-Verwendung k onnen von auen mit synchronisierten Anweisungen weitere wait()-/notify-Aufrufe hinzugef ugt werden falscher Thread wird aufgeweckt 3.7.2 Variante von wait() mit Timeout: wait(long) oder wait(long, int), wobei long die Zeit in Millisekunden und int die Zeit in Nanosekunden darstellen. Nach Ablauf der Zeit fordert der Thread eigenst andig wieder einen Lock an. Bei notify() ist kein Zeitablauf m oglich, wobei der Thread trotzdem aufgeweckt wird. wait(0) = wait(0,0) = wait()

13

3.7.3 Fallstudie: einelementiger Puer In dieser Fallstudie wollen wir eine Kommunikationsabstraktion in Java entwickeln: einen einelementigen Puer, welcher einen synchronisierten Zugri auf einen Wert wie folg bietet: Puer kann leer oder voll sein ein Wert kann in einen leeren Puer geschrien werden ( put()) aus einem vollen Puer kann ein Wert entfernt werden ( take()) take() suspendiert, falls der Puer leer ist put() suspendiert falls der Puer voll ist Der einelementige Puer kann auch als ver anderbare Variable gesehen werden. Wir nennen ih deshalb MVar (mutable variable).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27

p u b l i c c l a s s MVar <T> { private T content = null ; p r i v a t e b o o l e a n empty ; p u b l i c MVar ( ) { empty = true ; } p u b l i c MVar(T o ) { empty = f a l s e ; content = o ; } p u b l i c s y n c h r o n i z e d T t a k e ( ) throws I n t e r r u p t e d E x c e p t i o n { while ( empty ) { w a i t ( ) ; } notifyAll (); empty = true ; return content ; } p u b l i c s y n c h r o n i z e d v o i d put (T o ) throws I n t e r r u p t e d E x c e p t i o n { while ( ! empty ) { w a i t ( ) ; } notifyAll (); empty = f a l s e ; content = o ; } }

Bemerkungen: Zeile 1: das <T> ist eine Typvariable, d. h. eine Variable u ullt ber Typen, die mit Objekttypen gef werden kann wirkt als Polymorphie Zeile 9: das Argument ist hier ein Objekt o vom Typ T Zeile 16: hier werden die Schreiber aufgeweckt, da der Puer jetzt leer ist Zeile 23: hier werden die Leser aufgeweckt, da der Puer jetzt wieder voll ist Dieser Code ist unsch on, da er ein wenig zielgerichtetes Erwecken der suspendierten Threads implementiert. 1. Verbesserung Gruppierung der Threads in Gruppen, was ein Erwecken von jeweils nur relevanten Threads erm oglicht: (i) take()-Threads (ii) put()-Threads Listing 14: Verwendung von Synchronisationsobjekten zur zielgerichteten Erweckung der Threads
1 2

p u b l i c c l a s s MVar <T> { private T content = null ;

14

3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38

p r i v a t e b o o l e a n empty ; p r i v a t e Obj ect r = new Obj ect ( ) ; w = new Ob jec t ( ) ; p u b l i c MVar ( ) { empty = true ; } p u b l i c MVar(T o ) { empty = f a l s e ; content = o ; } p u b l i c T t a k e ( ) throws I n t e r r u p t e d E x c e p t i o n { synchronized ( r ) { while ( empty ) { r . w a i t ( ) ; } s y n c h r o n i z e d (w) { empty = true ; w. n o t i f y A l l ( ) ; return content ; } } } p u b l i c v o i d put (T o ) throws I n t e r r u p t e d E x c e p t i o n { s y n c h r o n i z e d (w) { while ( ! empty ) { w . w a i t ( ) ; } synchronized ( r ) { empty = f a l s e ; content = o ; notifyAll (); } } } }

Bemerkungen: Zeile 5/6: dies sind die beiden Synchronisationsobjekte Zeie 21: r.wait() darf nur ausgef uhrt werden, wenn man einen Lock auf r hat Zeile 23: hier bendet sich maximal 1 Thread und es gilt empty = false Zeile 26/27 dies stellt einen kritischen Bereich dar, der u ber r und wait() abgesichert ist Zeile 36: hier wird kein Lock gehalten Zeile 38: hier bendet sich maximal 1 Thread und es gilt empty = true Achtung: take() h alt ein Lock auf r und wartet ggf. auf w und put() h alt Lock auf w und wartet auf r! (race condition) Gefahr eines Deadlocks! Es ist aber kein Deadlock m oglich, da an relevanten Programmpunkten unterschiedliche Werte f ur empty vorliegen m ussen es k onnen keine zwei Threads gleichzeitig an diesem Punkt sein! 2. Verbesserung Da wir jetzt schon spezielle Lockobjekte zur Unterscheidung der Leser und Schreiber eingef uhrt haben, sollte es doch auch m oglich sein gezielt nur einen Leser bzw. Schreiber zu erwecken, d. h. notify() statt notifyAll() zu verwenden.
1 2 3 4 5 6

p u b l i c c l a s s MVar <T> { private T content = null ; p r i v a t e b o o l e a n empty ; p r i v a t e Obj ect r , w ;

15

Abbildung 1: Verwendung von MVars beim Producer-/Consumer-Problem


p u b l i c MVar ( ) { empty = true ; r = new O bjec t ( ) ; w = new O bjec t ( ) ; p u b l i c MVar(T o ) { empty = f a l s e ; content = o ; r = new O bjec t ( ) ; w = new O bjec t ( ) ;

7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39

p u b l i c T t a k e ( ) throws I n t e r r u p t e d E x c e p t i o n { synchronized ( r ) { while ( empty ) { r . w a i t ( ) ; } s y n c h r o n i z e d (w) { empty = true ; w. n o t i f y ( ) ; return content ; } } } p u b l i c v o i d put (T o ) throws I n t e r r u p t e d E x c e p t i o n { s y n c h r o n i z e d (w) { while ( ! empty ) { w . w a i t ( ) ; } synchronized ( r ) { empty = f a l s e ; content = o ; notify (); } } } }

Auf den ersten Blick scheint es auch m oglich, dieses Programm weiter zu vereinfachen und anstelle von while nur ein if zu verwenden. Dies ist aber nicht m oglich, wenn man sich nochmal die genaue Semantik des wait und notify Mechanismus anschaut. Bei notify wird ein schlafender Thread nicht unmittelbar erweckt und weiterlaufen. Vielmehr bewirbt sich der erweckte Thread um den Lock, welcher zun achst noch vom notify ausf uhrenden Thread gehalten wird. Dieser kann ihm aber von einem gerade neu initierten Leser (oder Schreiber) weggeschnappt werden, wodurch die Semantik der MVar verletzt w urde und ggf. zwei Threads einen Wert auslesen oder setzen w urden. Nach auen bieten MVars eine gut Abstraktionsstruktur, welche durchaus sinnvoll zur synchronisierten Threadkommunikation eingesetzt werden kannn. [] Verwende in Zukunft besser MVars an statt synchronized, wait() und notify(). Ahnliche Kommunikationsabstraktionen werden im Paket java.util.concurrent.atomic zur Verf ugung gestellt. Allerdings Java typisch mit sehr vielen Kontrollm oglichkeiten. Als Verwendungsbeispiel f ur MVars k onnen wir das Producer-/Consumer-Problem betrachten. Auf den ersten Blick scheint ein einelementiger Puer nicht besonders geeignet zu sein, da Produzenten blockiert werden. Dieses Konzept ist in der Praxis aber oft gut geeignet, da auch automatisch eine Lastbalancierung stattndet. Einem Uberangebot f uhrt zu einer Sequentialisierung auf Seiten der Producer, wodurch mehr Rechenzeit f ur die Consumer zur Verf ugung steht (siehe Abbildung 1).

16

Abbildung 2:

3.8 Beenden von Threadausf uhrungen


Die Methode isAlive() liefert true, falls der Thread noch l auft (auch wenn er gerade suspendiert ist). Beenden eines Threads durch: a) normales Beenden der run-Methode (durch Terminieren des Codes) b) Abbruch der run-Methode c) Aufruf der destroy-Methode (depricated) d) Ende aller Nicht-D amon-Threads Bei a) und b) werden alle Locks freigegeben Bei c) wird der Thread wirklich abgeschossen, ohne das Aufr aumarbeiten stattnden (destroy ist allerdings nicht immer implementiert und sollte nicht mehr verwendet werden). Alternative Interrupts (s.u.). d) ist unproblematisch, da wir ja sowieso am Programmende sind. Unterbrechung von Threads Es stehen folgende Methoden der Klasse Thread zur Verf ugung: public void interrupt() sendet einen Interrupt an den Thread, was zu einer InterruptedException in den Zust anden sleep und wait f uhrt public boolean isInterrpted() testet, ob der Thread ein Interrupt erhielt public static boolean interrupted() testet den aktuellen Thread auf einen Interrupt und l oscht ggf. den Interrupt-Zustand zum Beenden einer langen nebenl augen Berechnung (diese Methode kann ein Thread nur auf sich selbst anwenden) InterruptedExceptions k onnen entweder mittels try ... catch ... final aufgefangen oder weitergeleitet werden (mittels throws InterruptedException) in MVar put / read / take throws InterruptedException siehe Abbildung 2

3.9 Warten auf Ergebnisse


Wenn eine Threadberechnung terminiert, kann sie Ergebnisse andren Threads zur Verf ugung, z. B. als Attribut oder u onnte man isAlive() ber spezielle Selektor-Methoden. Um dies zu synchronisieren k und busy waiting verwenden. D.h. Threads welche auf das Ergebnis eines anderen Threads warten, u ufen ob der Thread noch lebt und fahren fort, wenn dies nicht mehr der Fall ist. Dies verberpr schwendet aber Systemressourcen. Alternative 1: Kommunikation und Synchronisation u ber MVars Alternative 2: join-Methode der Thread-Klasse. Ein Thread, der die join-Methode eines anderen Threads aufruft, suspendiert, bis der andere Thread terminiert. Listing 15: Beispiel
1 2 3 4

c l a s s Calc e x t e n d s Thread { private int result ; p u b l i c v o i d run ( ) { r e s u l t = . . . ; } public int getResult () { return r e s u l t ; }

17

5 6 7 8 9 10 11 12 13 14 15 16

} c l a s s showJoin { v o i d run ( ) { Calc c a l c = new Calc ; calc . start (); try { c a l c . join ( ) ; System . out . p r i n t l n ( R e s u l t i s + c a l c . g e t R e s u l t ( ) ) ; } catch ( InterruptedException e ) { . . . } } }

Wie sleep() und wait() kann join() durch einen Interrupt unterbrochen werden. Die Semantik von join() ist ahnlich: while(calc.isAlive()) { wait(); } mit zugeh origem notifyAll(); bei Terminierung. Variante mit Timeout join(long Nanosekunden) join(long Nanosekunden, int Millisekunden) Der Grund f ur die Wiedererweckung (Terminierung oder Zeitablauf) ist nicht unterscheidbar. Durch ein zus atzliches isAlive() kann dies herausgefunden werden. Weitere Konzepte Lese- und Schreiboperationen auf int und boolean sind atomar. Die Kombination von mehreren aber nicht, genauso wie dies nicht f ur long und double gilt. Dies kann ggf. zu inkonsistenten Werten f uhren (z. B. wenn die erste H alfte geschrieben wurde) L osung: Synchronisieren oder Werte als volatile deklarieren atomare Ver anderungen weiteres Problem: Compileroptimierung Wir betrachten einen Thread, der den in Listing 3.9 ausf uhrt. Wenn nun andere Threads den Wert der Variablen currentValue ver andern, so sollte sich auch die Ausgabe des Threads andern. Listing 16: Beispiel
1 2 3 4 5

currentValue = 5; while ( true ) { System . out . p r i n t l n ( c u r r e n t V a l u e ) ; Thread . sleep ( 1 0 0 0 ) ; }

Dies ist aber nicht unbedingt der Fall und es ist m oglich, dass weiterhin nur 5 und nicht der neu gesetzte Wert ausgegeben wird. Dies liegt an einer Compileroptimierung: Konstantenpropagation. Der Wert der Variablen currentValue wird vom Compiler direkt in das System.out.println() hinein geschrieben wird, da der Wert ja immer gleich ist. Als L osung sollte die Variable currentValue als volatile deklariert werden, was eine Compileroptimierung verhindert.

3.10 ThreadGroups
Threads k onnen in Gruppen zusammengefasst werden, was die Strukturierung von Threadstrukturen erm oglicht. Es handelt sich um eine hierarchische Struktur, was bedeutet, dass ThreadGroups sowohl Threads als auch ThreadGroups enthalten k onne. Prinzipiell geh ort jeder Thread zu einer ThreadGroup. Standardm aig ist dies die gleiche ThreadGroup wie die des Elternthreads. Die ThreadGroup kann ver andert werden mittels public Thread(ThreadGroup g [, String name]), wobei der Name optional ist. Nun ThreadGroups k onnen mittels public ThreadGroup(String name) als Untergruppe zur aktuellen Threadgruppe generiert werden. ThreadGroups bieten folgende Methoden:

18

getName() getParent() setDaemon(boolean daemon) setMaxPriority(int maxPri) int activeCount() gibt die Anzahl der Threads aus; die Gruppe bekommt man mittels isAlive() int enumerate(Thread[] threadisInGroup.boolean recursive), wobei das int die Anzahl der Threads, Thread[] die Threads der Gruppe und boolean = true ist (auch bei Untergruppen) Die Semantik ist hier etwas anders als bei Threads: ThreadGroups werden gel oscht, wenn sie leer sind. Auerdem gibt es SecurityHandler auf Gruppen (z. B. Wer darf Thread in ThreadGroups erzeugen?)

ThreadPools
Fast das gleiche Konzept wie ThreadGroups gibt es im Paket java.util.concurrent mit den ThreadPool nocheinal. Hierbei handelt es sich allerdings um eine Abstraktion zur Verwaltung von laufenden Threads. Sie erm oglichen eine Einsparung von System-Ressourcen durch Wiederverwendung von Threads.

Executer Interface
Das Executer Interface stellt M oglichkeiten zum Ausf uhren von Threads im Rahmen von ThreadPools zur Verf ugung: void execute(Runnable o) nimmt einen Task (das Runnable o / nicht Thread!) in einen ThreadPool auf; also e.execute(r) mit e als Executer-Objekt und r als Runnable-Objekt statt new(Thread(r)).start() die Klasse Executer stellt statische Methoden zur Generierug von Executoren zur Verf ugung: static ExecuterService new FixedThreadPool(int n) startet den ThreadPool mit n Threads und stellt eine Verfeinerung von Executer dar. Ein neuer Task (mit execute hinzugef ugt) wird von einem dieser Threads ausgef uhrt, falls ein Thread verf ugbar ist. Ansonsten wird er in eine Warteschlange eingef ugt und gestartet, falls ein anderer Task beendet wird kein Overhead durch Threadstart. Die Threads werden erst beim shutdown beendet. es sind alternative Methoden verf ugbar (z. B. zum bedarfsgesteuerten Starten von Threads [mit Obergrenze] oder dynamischen ThreadPools)

Debugging
Es gibt nur eingeschr ankte Infomationen, die f ur ein printf-Debugging genutzt werden k onnen: die Klasse Thread stellt einige Methoden zum Debuggen zur Verf ugung: public String toString() liefert Stringrepr asentation des Thread inklusive Name, Priorit at und ThreadGroup public static void dumpStack() liefert einen Stacktrace des aktuellen Threads auf System.out f ur ThreadGroups gibt es: public String toString() public void list() druckt rekursiv die toString()-Ergebnisse aller Threads/ ThreadGroups aus

Beurteilung von Java Threads


Kommunikation u ber geteilten Speicher (Objekte)

19

Abbildung 3: gute Kapselung des wechselseitigen Ausschlusses in Obkjekten mittels synchronized Synchronisation der Kommunikation mittels wait(), notify() und notifyAll(), was allerdings sehr ungerichtet ist meistens Verwendung von notifyAll() und busy waiting viele zus atzliche Konzepte: Priorit aten, D amonen, sleep()/yield(), Server-Side- und ClientSide-Synchronisation mittels synchronized-Ausdr ucken, Interrupts, isAlive(), join(), ThreadGroups, ThreadPools, Synchronisation auf Klassenebene, . . . Vererbung und synchronized-Methoden passen nicht gut zusammen Kommunikationsabstraktion (MVar, Chan oder entsprechende Konstrukte in java.util.concurrent) ersetzen inzwischen viele andere Konzepte und erm oglichen eine Programmierung auf h oherem Niveau (Message Passing). Auch hier gibt es aber wieder tauschende an Kongurationsm oglichkeiten (z. B. timeouts).

Unbeschr ankter Puer (Chan)


Ein Chan dient zur Kommunikation zwischen Schreibern und Lesern, so dass die Schreiber nie suspendieren. Implementierung mittels verketteter Liste Leser m ussen ggf. suspendieren, falls der Chan leer ist. Leser und Schreiber m ussen synchronisiert werden. Zur Implementierung (der Zeiger) wollen wir MVars verwenden (siehe Abbildung 3).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25

c l a s s Chan<T> { p r i v a t e MVar<MVar<ChanElem<T>>> read , w r i t e ; p r i v a t e c l a s s ChanElem<T> { private T value ; p r i v a t e MVar<ChanElem<T>> next ; p u b l i c ChanElem (T v , MVar<ChanElem<T>> n ) { value = v ; next = n ; } public T value () { return value ; } p u b l i c MVar<ChanElem<T>> next ( ) { r e t u r n next ; } } p u b l i c Chan ( ) throws I n t e r r u p t e d E x c e p t i o n { MVar<ChanElem<T>> h o l e = new MVar<ChanElem<T> > (); r e a d = new MVar<MVar<ChanElem<T>>>(h o l e ) ;

20

Abbildung 4:
w r i t e = new MVar<MVar<ChanElem<T>>>(h o l e ) ; } p u b l i c T r e a d ( ) throws I n t e r r u p t e d E x c e p t i o n { MVar<ChanElem<T>> rEnd = r e a d . t a k e ( ) ; ChanElem<T> item = rEnd . r e a d ( ) ; r e a d . put ( item . next ( ) ) ; r e t u r n item . v a l u e ( ) ; } p u b l i c v o i d w r i t e (T o ) { MVar<ChanElem<T>> newHole = new MVar<ChanElem<T>> (), oldHole = write . take ( ) ; o l d H o l e . put ( new ChanElem<T>(o , newHole ) ) ; w r i t e . put ( newHole ) ; } }

26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42

Bemerkungen: Zeile 26: hier ndet eine Synchronisation der Leser statt, da durch read die MVar kurzzeitig leer wird, was dann andere Leser suspendiert Zeile 27: liest die erste MVar s und liefert das erste Element (v1 ) Zeile 28: hier wird die Blockade der anderen Leser wieder aufgehoben Zeile 34: hier werden die anderen Schreiber suspendiert Siehe Abbildung 4 Beachte: Die Implementierung erm oglicht gleichzeitiges Lesen und Schreiben des nicht-leeren Chans! Als Erweiterung k onnen wir zum Chan noch eine Methode isEmpty hinzuf ugen, welche den aktuellen Zustand des Chans analysiert. Sicherlich muss bei der Verwendung einer solchen Methode bedacht werden, dass erhaltene Information nur eine sehr begrenzte G ultigkeit hat. Man kann sich aber dennoch F alle vorstellen, in denen solch eine Methode n utzlich sein kann.
1 2 3 4 5 6 7

p u b l i c b o o l e a n isEmpty ( ) { MVar<ChanElem<T>> rEnd = r e a d . t a k e ( ) ; MVar<ChanElem<T>> wEnd = w r i t e . t a k e ( ) ; w r i t e . put (wEnd ) ; r e a d . put ( rEnd ) ; r e t u r n ( rEnd==wEnd ) ; }

In der Ubung wird die MVar-Implementierung um eine read-Methode, welche die Mvar nicht leert erweitert. Mit dieser Methode ist eine Vereinfachung dieser Implementierung m oglich. Beachte aber, dass diese Implementierung nicht in allen Situationen das erwartete Verhalten hat. Durch die Synchronisierung mit anderen lesenden Threads, kann es dazu kommen, dass isEmpty blockiert, n amlich genau dann, wenn ein anderer Thread bereits von einem leeren Chan lesen will und hierbei nat urlich suspendiert. L osungsideen f ur dieses Problem werden in den Ubungen besprochen. Eine weitere L osung werden wir bei abstrakteren Konzepten sp ater in dieser Vorlesung kennen lernen.

21

Noch ein Konzept: Pipes (oder ein alter Schritt in Richtung Message Passing)
Pipes werden meist f ur Datei- oder Netzwerkkommunikation verwendet. Mit ihnen ist aber auch eine Kommunikation zwischen Threads m oglich. Eine Pipe besteht aus zwei Str omen: (i) PipedInputStream (ii) PipedOutputStream , welche bei der Konstruktion durch: PipedOutputStream out = new PipedOutputStream(); PipedInputStream in = new PipedInputStream(out); oder mittels in.connect(out) verbunden werden k onnen. Pipes haben in der Regel eine beschr ankte Gr oe. Threads, die aus einer leeren Pipe lesen oder in eine volle Pipe schreiben wollen, werden suspendiert. Das Schreiben erfolgt mittels: write(int b) write(byte[], int off, int len) schreibt b[of f ] . . . b[of f + len] flush() setzt die gepuerte write-Methode um close() schliet das Schreibende das Lesen mittels: int read() leifert -1, falls die Pipe geschlossen ist int read(byte[], int off, int len) suspendiert bei einer unzureichenden Zahl an Bytes auf der Pipe int available() liefert die Anzahl der Bytes in der Pipe close() Andere Daten als Bytes k onnen mittels DataOutputStream oder PipeWriter u bertragen werden. Ein Vergleich mit Chan und MVar ndet in der Ubung statt.

4 Nebenl auge Programmierung in Haskell


Auch Haskell bietet in Form der Bibliothek Concurrent Haskell die M oglichkeit nebenl aug zu programmieren. Hierbei wird eine MVar, wie wir sie in Java implementiert haben die grundlegende Datenstruktur dar. Eine Einf uhrung in die relevanten Haskell wird im Rahmen der Ubungen behandelt.

4.1 Concurrent Haskell


Erweiterung von Haskell 98 um Konzepte zur nebenl augen Programmierung. Modul: Control.Concurrent. Zur Generierung neuer Threads steht die Funktion forkIO :: IO() -> IO ThredId zur verf ugung. Das Argument vom Typ IO() stellt den abzuspaltenden Prozess dar. Die Aktion ist sofort beendet. Die ThredId identiziert den neu abgespalteten Thread. Ein Thread kann seine eigene ThreadId mit folgenden Funktion abfragen: myThreadId :: IO ThreadId Threads werden bei Terminierung beendet oder mittels killThread::ThreadId->IO() abgeschos sen. Im Gegensatz zu Erlang werden die ThreadIds aber nicht zur Kommunikation verwendet. Listing 17: Beispiel
1 2 3 4

main : : IO ( ) main = do i d < f o r k I O ( l o o p 0 ) s t r < g e t L i n e

22

5 6 7 8 9 10 11 12

i f s t r == s t o p then k i l l T h r e a d i d else return () l o o p : : I n t >IO ( ) l o o p n = do print n l o o p ( n+1)

Das Beispiel ist nicht ganz so sch on, da das gesamte Programm eh terminiert, falls der Hauptthread terminiert.

4.2 Kommunikation
Haskell bietet Kommunikationsabstraktionen namens MVar und Chan, wie wir sie auch schon in Java kennengelernt (und dort auch implementiert) haben. newEmptyMVar :: IO(MVar a) kreiert eine neue leere MVar newMVar :: a -> IO(MVar a) kreiert neue gef ullte MVar. takeMVar :: MVar a -> IO a putMVar :: Mvar a - > a -> IO() readMVar :: MVar a -> IO a tryPutMVar :: Mvar a -> a -> IO Bool ... Dinierende Philosophen Listing 18: Dinierende Philosophen
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27

p h i l : : MVar ( ) > MVar ( ) > IO ( ) p h i l l r = do takeMVar l takeMVar r e a t putMVar l ( ) putMVar r ( ) phil l r main : : IO ( ) main = do s t i c k s < c r e a t e S t i c k s 5 startPhils sticks p h i l ( l a s t s t i c k s ) ( head s t i c k s ) c r e a t e S t i c k s : : I n t > IO ( [ MVar ( ) ] ) createSticks 0 = return [ ] c r e a t e S t i c k s n = do s t i c k s < c r e a t e S t i c k s ( n 1) s t i c k < newMVar ( ) return ( stick : s t i c k s ) s t a r t P h i l s : : [ MVar ( ) ] > IO ( ) startPhils [ ] = return () s t a r t P h i l s ( l : r : s t i c k s ) = do forkIO ( p h i l l r ) startPhils ( r : sticks )

--eat ist ein Kommentar [_] matched eine ein-elementige Liste

23

4.3 Kan ale in Haskell


Da die MVar als grundlegende Datenstruktur auch in Haskell vorhanden ist, ist eine Ubertragung der Chan-Implementierung recht einfach m oglich:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39

module Chan where import C o n t r o l . Concurrent . MVar data Chan a = Chan (MVar ( Stream a ) ) r e a d end (MVar ( Stream a ) ) w r i t e end ty pe Stream a = MVar ( ChanElem a ) data ChanElem a = ChanElem a ( Stream a ) newChan : : IO ( Chan a ) newChan = do h o l e < newEmptyMVar r e a d < newMVar h o l e w r i t e < newMVar h o l e r e t u r n ( Chan r e a d w r i t e ) writeChan writeChan newHole oldHole putMVar putMVar : : Chan a > a > IO ( ) ( Chan w r i t e ) v = do <newEmptyMVar < takeMVar w r i t e o l d H o l e ( ChanElem v newHole ) w r i t e newHole

readChan : : Chan a > IO a readChan ( Chan r e a d ) = do rEnd < takeMVar r e a d ( ChanElem v newReadEnd ) < readMVar rEnd putMVar r e a d newReadEnd return v isEmptyChan : : Chan a > IO Bool isEmptyChan ( Chan r e a d w r i t e ) = do rEnd < takeMVar r e a d wEnd < takeMVar w r i t e putMVar w r i t e wEnd putMVar r e a d rEnd r e t u r n ( rEnd==wEnd)

Die Verwendung von Kan alen erm oglicht einen anderen Programmierparadigma, das so genannte M essage Passing. Message Passing wird z.B. in Erlang und Scala zur nebenl augen und auch verteilten Programmierung eingesetzt. Im folgenden wollen wir uns zun achst intensiever mit Erlang auseinandersetzen.

5 Erlang
Erlang wurde von der Firma Ericsson entwickelt. Es ist eine funktionale Programmiersprache mit speziellen Erweiterungen zur nebenl augen und verteilten Programmierung. Sie wurde gezielt zur Entwicklung von Telekommunikationsanwendungen eintwickelt, wird inzwischen aber auch in anderen Bereichen zur Entwicklung insbesondere verteilter Anwendungen eingesetzt.

5.1 Sequentielle Programmierung in Erlang


Erlang ist eine funktionale Programmiersprache mit strikter Auswertung, d. h. es werden zuerst die Argumente ausgewertet und erst anschlieend die Funktion aufgerufen, wie in imperativen Sprachen

24

auch. Erlang ist ungetypt/(dynamisch) getypt Laufzeittypfehler f ur unpassende Basistypen (z. B. true + 7 Widerspruch). Erlang ist verf ugbar unter www.erlang.org und auf den SUNs unter /home/erlang/bin/erl installiert. Listing 19: Fakult atsfunktion (factorial) als Beispiel
1 2

f a c ( 0 ) > 1 ; f a c (N) when N>0 > N f a c (N 1).

Erkl arungen: ; trennt die Regeln der Funktions-Denition Variablen werden gro geschrieben der guard when N>0 geh ort zur Auswahl der Regel mit hinzu jede Funktionsdenition endet mit einem Punkt. Statt let-Ausdr ucken bind-once-Variablen:
1 2 3

f a c (N) when N>0 > N1 = N 1, F1 = f a c (N1 ) , N F1 .

Erkl arungen: , als Trennzeichen der Sequenzen die letzte Zeile ist das Ergebnis der Berechnung, welches automatisch ausgegeben wird Funktionen werden nach ihrer Stelligkeit unterschieden: fac(N,M) -> ... ist eine andere Funktion als fac/1, was f ur eine ein-stellige Funktion wie z. B. fac(N) steht. jedes Erlang-Programm ben otigt eine Modulkopf: -modul(math). das Modul bendet sich in der Datei math.erl -export([fac/1]). erlaubt den Zugri von auen Ablauf: > erl ... > c(math). > math:fac(6). 720 Erkl arungen: c(math). compiliert das Modul math in der Datei math.erl. Alternativ kann in der Shell mittels erlc math.erl kompiliert werden. halt(). oder Crtl + Cund dann a f uhrt zum Verlassen der Erlang-Umgebung. Datenstrukturen/Werte: Atome: a, b, c, 0, 1 ,true, false, ..., Hallo, willi@mickey beginnen mit Kleinbuchstaben oder stehen in Hochkommata. Tupel: { t1 , . . . , tn } mit ti als beliebigem Wert, z. B. B aume {node,{node,empty,1,empty},2,empty} Listen: [e|l], wobei e das Kopfelement der Liste ist. l ist eine Restliste und [] steht f ur die leere Liste. Als Abk urzung k onnen wir auch [1,2,3] statt [1|[2|[3|[]]]] schreiben. Beispiel: Konkatenation zweier Listen app([], Ys) -> Ys; app([X|Xs], Ys) -> [X|app(Xs, Ys)]. Patern Matching: Pat = e, wobei Pat eine der folgenden Formen hat: Variable Atom

25

Tupel {P at1 , . . . , P atn } [ P at1 |P ate ] [] [ P at1 , . . . , P atn ] Das Matching eines Pattern bindet ungebundene Variablen an Teilstrukturen der Werte; z.B: {X,[1|Ys]} = {42,[1,2,3]} das X gebunden wird {X,{1,Y},Y} = {3,{1,[]},42} m usste {X,{1,Y},Y} = {3,{1,42},42} [X/42, Ys/[2,3]], wobei X/42 bedeutet, dass die 42 an macht nicht, da Y an [] und 42 gebunden werden [X/3, Y/42]

Beachte: Die Substitution wird auf alle vorkommenden Variablen angewendet, auch in Pattern. Ein Umbinden ist nicht mehr m oglich! Beispiel: {X,[Y|Ys]} = {42,[1,2]} [X|Xs] = [3,4] wobei die zweite Zeile zum Fehler f uhrt, da X bereits an 42 gebunden ist.

5.2 Nebenl auge Programmierung


Erlang-Prozesse sind light-weight, so dass davon gleichzeitig sehr viele existieren k onnen. Ein Erlang-Prozess besteht aus folgenden drei Bestandteilen: Programmterm t, der ausgeweret wird Prozess-Identier pid Mailbox (Message Queue), die Nachrichten aufnimmt Starten von Erlang-Prozessen: spawn(module, func,[a1 , . . . , an ]) startet einen nebenl augen Prozess, dessen Berechnung mit module: func(a1 , . . . , an ) startet spawn terminiert sofort und liefert den pid des neuen Prozesses zur uck Beachte: func muss in module exportiert werden (auch wenn spawn im selben Modul ausgerufen wird) Seinen eigenen pid erh alt ein Prozess durch Aufruf von self(). Senden von Nachrichten: pid!v sendet den Wert v an den Prozess pid. Der Wert wird hinten in die Mailbox von pid eingetragen. pids sind Werte wie Atome oder Listen und k onnen somit auch in Datenstrukturen gespeichert oder auch verschickt werden es ist gew ahrleistet, dass Nachrichten nicht verloren gehen Empfangen von Nachrichten: receive-Ausdruck
1 2 3 4 5 6

receive Pat1 > e1 ; ... Patn > e n [ a f t e r t > e ] end

die Pattern werden der Reihe nach gegen die Werte der eigenen Mailbox gematcht (exakte Reihenfolge siehe Ubung) erstes passendes Pattern P ati wird gew ahlt ( Substitution ). Das gesamte receive-Statement wird durch (ei ) ersetzt. Die Berechnung macht mit ei weiter. bei after t -> e ndet ein Timeout nach t Millisekunden statt und es geht mit e weiter Spezialfall: t = 0 die Pats werden nur genau einmal gegen alle Mailbox-Eintr age gematcht

26

5.3 Ein einfacher Key-Value-Store


Listing 20: Server des Key-Value-Stores
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

module ( dataBase ) . e x p o r t ( s t a r t / 0 ) . s t a r t ( ) > dataBase ( [ ] ) . dataBase (KVs) > receive { a l l o c a t e , Key , P} > case lo oku p ( Key , KVs) o f n o t h i n g > P ! f r e e , r e c e i v e { v a l u e , V, P} > dataBase ( [ { Key ,V } | KVs ] ) end ; { j u s t ,V} > P ! a l l o c a t e d , dataBase (KVs) end ; { lookup , Key , P} > P ! lo oku p ( Key , KVs ) , dataBase (KVs) end . l o o k u p (K , [ ] ) > n o t h i n g ; l o o k u p (K, [ { K,V } | ] ) > { j u s t ,V } ; l o o k u p (K, [ | KVs ] ) > lo oku p (K, KVs ) ;

Listing 21: Beispiel-Client zum Key-Value-Stores


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29

module ( c l i e n t ) . e x p o r t ( [ s t a r t / 0 ] ) . import ( base , [ getLn / 1 , p r i n t L n / 1 ] ) s t a r t ( ) > DB = spawn ( database , s t a r t , [ ] ) , c l i e n t (DB) . c l i e n t (DB) > I n p u t = getLn ( ( l ) ookup / ( i ) n s e r t > ) , case I nput o f i > K = getLn ( key > ) , DB! { a l l o c a t e , K, s e l f ( ) } , receive f r e e > V = getLn ( v a l u e > ) , DB! { v a l u e , V, s e l f ( ) } ; a l l o c a t e d > p r i n t L n ( key a l l o c a t e d ) end ; l > K = getLn ( key > ) , DB! { lookup , K, s e l f ( ) } , receive n o t h i n g > p r i n t L n ( Key not a l l o c a t e d ) ; { j u s t , V} > p r i n t L n (V) end ; X > p r i n t L n (X) end , c l i e n t (DB) .

Bemerkungen: Zeile 3: base ist ein Modul mit IO-Funktionen (wird auf der Web-Site bereit gestellt) Zeile 6: DB enth alt den zugeh origen Prozess-Identier Zeile 27: dieser Fall f angt alle sonst-F alle ab. Hier kann auch _ -> 42 stehen. Beachte: Es k onnen auch mehrere Clienten mit einem Server kommunizieren. Dies entspricht einem Nameserver, kann aber auch als geteilter Speicher gesehen werden.

27

Dieser Key-Value-Store stellt zum einen eine einfache erste Anwendung zur nebenl augen Programmierung in Erlang dar. Er zeigt aber auch, dass man mit Hilfe eines Server-Prozesses und Message Passing, das Modell eines geteilten Speichers simulieren kann. Der Schl ussel entspricht hierbei der Speicheradresse und der Wert dem abgelegten wert. Im n achsten Schritt sollten wir also untersuchen, ob auch ahnliche Synchronisationsmechanismen, wie sie bei geteiltem Speicher verwendet werden, simuliert werden k onnen.

5.4 Wie k onnen Prozesse in Erlang synchronisiert werden?


Als Beispiel: Implementierung der dinierenden Philosophen Idee: Repr asentiere den Zustand eines Sticks innerhalb eines speziellen Stick-Prozesses. Die Funktionen zum Nehmen und Hinlegen eines Essst abchens k onnen dann wie folgt deniert werden: Erlang-Prozesse sind seiteneektsfrei, bis auf das Senden von Nachrichten! Listing 22: Essst abchen
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18

stickDown ( ) > receive { take , P} > P ! took , stickUp ( ) . > stickDown ( ) end . s t i c k U p ( ) > receive put > stickDown ( ) end . t a k e ( St ) > St ! { take , s e l f ( ) } , receive took > ok end . put ( St ) > St ! put .

Hierbei ist zu beachten, dass ein entsprechender Stabprozess zwischen den beiden Zust anden stickUp() und stickDown() hin- und herwechselt. Die take-Nachricht wird aber nur im stickDown()-Zustand verarbeitet. Bei verb+stickUp()+ verbleibt sie in der Mailbox. Auerdem wartet der Prozess, der take/2 ausf uhrt im receive, bis eine entsprechende Best atigungsnachricht (took) verschickt wurde. Nun ist es einfach, die Philosophen zu realisieren: Listing 23: Dinierende Philosophen
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21

module ( d i n i n g P h i l s ) . e x p o r t ( [ s t a r t / 1 , stickDown ( ) / 0 , p h i l / 3 ] ) . s t a r t (N) when N >= 2 > S t i c k s = c r e a t e S t i c k s (N) , startPhils ([ l i s t s : last ( Sticks )| Sticks ] , 0). c r e a t e S t i c k s ( 0 ) > [ ] ; c r e a t e S t i c k s (N) > S t i c k = spawn ( d i n i n g P h i l s , stickDown , [ ] ) , S t i c k s = c r e a t e S t i c k s (N 1) , [ Stick | Sticks ] . s t a r t P h i l s ( [ ] , N) > ok ; s t a r t P h i l s ( [ SL , SR | S t i c k s ] , N) > spawn ( d i n i n g P h i l s , p h i l , [ SL , SR , N] ) , s t a r t P h i l s ( [ SR | S t i c k s ] , N+1). p h i l ( SL , SR , N) > b a s e : print (N) ,

28

22 23 24 25 26 27 28 29 30

base : printLn ( i s thinking ) , t i m e r : s l e p (10+N) , // #4 t a k e ( SL ) , t a k e (SR ) , b a s e : print (N) , base : printLn ( i s e a t i n g ) , put ( SL ) , put (SR ) , p h i l ( SL , SR , N ) .

Bemerkungen: Zeile 2: stickDown()/0 und phil/3 m ussen exportiert werden, damit diese gestartet werden k onnen Zeile 23: die Zeile ist zur direkten Deadlockvermeidung notwendig An diesem Beispiel erkennt man gut das Prinzip der Synchronisation in Erlang. Als weiteres Beispiel betrachten wir noch die Implementierung einer MVar in Erlang.

5.5 MVar
Als weiteres Beispiel wollen wir eine MVar in Erlang implementieren. Hierbei k onnen wir genau wie bei der Stick-Implementierung vorgehen. Wir m ussen nur zus atzlich einen Wert als Zustand des MVarProzesses vorsehen. Listing 24: MVar in Erlang
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26

module (mVar ) . e x p o r t ( [ new / 0 , new / 1 ,mVar/ 1 , t a k e / 1 , put / 2 ] ) . new ( ) > spawn (mVar , mVar , [ empty ] ) . new (V)> spawn (mVar , mVar , [ { f u l l ,V } ] ) . mVar( empty ) > r e c e i v e { put , V, P} > P ! put , mVar( { f u l l ,V} ) end ; mVar( { f u l l ,V} ) > r e c e i v e { take , P} > P ! { took ,V } , mVar( empty ) end . t a k e (MVar) > MVar ! { take , s e l f ( ) } , receive { took ,V} > V end . put (MVar ,V) > MVar ! { put , V, s e l f ( ) } , receive put > ok end .

Im Gegensatz zur Implementierung der St abchen, verwenden wir hier keine zwei sich abwechselnd aufrufenden Funktionen, sondern unterschieden die Zust ande durch zwei unterschiedliche Argumente (empty und {full,...}).

5.6 Nebenl auge Programmierung in Erlang


Wir haben nun gesehen, wie man geteilten Speicher und lockbasierte Synchronisation in Erlang simulieren kann. In der Praxis greift man allerdings selten auf diese Ansatz zur uck. Vielmehr verwendet man ausgezeichnete Server-Prozesse, welche die Anfragen an ein Objekt sequentialisieren und hierdurch den geteilten Zustand sch utzen.

29

Hier folgen noch zwei Beispielprogramme.

5.7 Verteilte Programmierung in Erlang


Nebenl auge Prozesse haben keinen gemeinsamen Speicher und k onnen deshalb problemlos auf mehreren Rechnern verteilt werden. Nachrichten werden automatisch in TCP-Nachrichten umgewandelt. (siehe Abbildung 5). Es ist auch m oglich, dass mehrere Knoten auf einem Rechner laufen. Starten eines Erlang-Knotens: erl -name willi da ansonsten keine veteilte Kommunikation m oglich ist, da der Prozess nicht bekannt ist Alternative: erl -sname willi analog, funktioniert aber nur im gleichen Subnetz Starten von Prozessen auf anderen Knoten: Voraussetzung: Erlang-Shell muss auf dem anderen Knoten/Rechner laufen z. B. DB = spawn(willi@mickey.informatik.uni-kiel.de, database, start, []) in obigem Beispiel allgemein: spawn(Knoten, module, func, args) wobei das funktionale Ergebnis wieder eine PID ist, welche jetzt auch IP-Adresse und Knotennamen mitkodiert. Dies eignet sich zum Auslagern von Berechnungen (z. B. zur Lastverteilung).

5.8 Verbinden unabh angiger Erlang-Prozesse


In oenen Systemen (z. B. Telefon, Chat) ist es n otig, Verbindungen zur Laufzeit herzustellen. Dazu ist das globale Registrieren eines Prozesses auf einem Knoten notwendig: register(name, pid). Senden an restliche Prozesse: {name, knoten}! msg, was meistens nur f ur den Erstkontakt verwendet wird. Danach ndet ein Austausch der PIDs statt (z. B. Database-Server l auft auf Knoten servermickey erl -sname server auf mickey). 5.8.1 Ver anderungen des Clients Listing 25: Ver anderung des Clients
1 2 3 4 5

s t a r t > { dbServer , s e rv e r @m i c ke y } ! { connect , s e l f ( ) } , receive { c onnected , DB} > c l i e n t (DB) end .

5.8.2 Ver anderungen des Servers Listing 26: Ver anderung des Servers
1 2 3 4 5 6 7 8 9 10 11

s t a r t ( ) > r e g i s t e r ( dbServer , s e l f ( ) ) , database ( [ ] ) . d a t a b a s e (L) > receive { a l l o c a t e , . . . } > . . . ; { lookup , . . . } > . . . ; { connect , P} > P ! { connected , s e l f ( ) } , d a t a b a s e (L) end .

Es sind also nur wenige Ver anderungen beim Ubergang von nebenl auger zu verteileter Programmierung notwendig. Als weiteres Beispiel wollen wir einen verteilten Chat implementieren, der aus einem (registrierten) Server und beliebig vielen passenden Clients besteht.

30

Abbildung 5: Listing 27: Ver anderung des Clients


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47

module ( c h a t S e r v e r ) . e x p o r t ( [ s t a r t / 0 ] ) . s t a r t ( ) > r e g i s t e r ( chat , s e l f ( ) ) , run ( [ ] ) . run ( C l i e n t s ) > receive { connect , Pid , Name} > case l oo k up 2 (Name , C l i e n t s ) o f n o t h i n g > Pid ! { connected , names ( C l i e n t s ) , s e l f ( ) } , b r o a d c a s t ( C l i e n t s , { l o g i n , Name } ) , run ( [ { Pid , Name } | C l i e n t s ] ) ; { j u s t , } > Pid ! nameOccupied , run ( C l i e n t s ) end ; { message , Nonsens , Pid } > case lo oku p ( Pid , C l i e n t s ) o f { j u s t , Name} > b r o a d c a s t ( C l i e n t s , { msg , Name , Nonsens } ) , run ( C l i e n t s ) ; n o t h i n g > run ( C l i e n t s ) end ; { l o g o u t , Pid } > case lo oku p ( Pid , C l i e n t s ) o f { j u s t , Name} > R e m a i n i n g C l i e n t s = remove ( Pid , C l i e n t s ) , b r o a d c a s t ( R e m a i n i n g C l i e n t s , { l o g o u t , Name } ) , run ( R e m a i n i n g C l i e n t s ) ; n o t h i n g > run ( C l i e n t s ) end end . names ( [ ] ) > [ ] ; names ( [ { , Name } | C l i e n t s ] ) > [ Name | names ( C l i e n t s ) ] . %names ( C l i e n t s ) > l i s t s : map( fun ( { ,X})>X end , C l i e n t s ) . b r o a d c a s t ( [ ] , ) > ok ; b r o a d c a s t ( [ { Pid , } | C l i e n t s ] , Msg ) > Pid ! Msg , b r o a d c a s t ( C l i e n t s , Msg ) . l o o k u p ( , [ ] ) > n o t h i n g ; l o o k u p (K, [ { K,V } | ] ) > { j u s t ,V } ; l o o k u p (K, [ | KVs ] ) > lo oku p (K, KVs ) . l o o k u p 2 ( , [ ] ) > n o t h i n g ; l o o k u p 2 (V, [ { K,V } | ] ) > { j u s t ,K } ;

31

48 49 50 51 52

l o o k u p 2 (V , [

| KVs ] ) > l oo k up 2 (V, KVs ) .

remove ( , [ ] ) > [ ] ; remove (K, [ { K, } | KVs ] ) > remove (K, KVs ) ; remove (K, [ KV | KVs ] ) > [KV | remove (K, KVs ) ] .

Listing 28: Ver anderung des Clients


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26

module ( c h a t C l i e n t ) . e x p o r t ( [ c o n n e c t / 2 , keyboard / 2 ] ) . c o n n e c t (Name , Node ) > { chat , Node } ! { connect , s e l f ( ) , Name } , receive { c onnected , Names , S e r v e r P i d } > b a s e : print ( Names ) , spawn ( c h a t C l i e n t , keyboard , [ S e r v e r P i d , Name ] ) , run ( S e r v e r P i d ) ; nameOccupied > 42 end . run ( S e r v e r P i d ) > receive ??? > . . . end .

keyboard ( S e r v e r P i d , Name) > S t r = b a s e : getLn (Name ) , case S t r o f bye > S e r v e r P i d ! { l o g o u t , s e l f ( ) } ; > S e r v e r P i d ! { message , Str , s e l f ( ) } , keyboard ( S e r v e r P i d , Name) end .

5.9 Robuste Programmierung


In Erlang sterben Prozesse normalerweise unbemerkt; Nachrichten an terminierte Prozesse gehen ohne Fehlermeldungen verloren. Prozesse k onnen aber auch mittels link(pid) verbunden werden. Durch einen Link wird eine bidirektionale Verbindung zwischen dem ausf uhrendem Prozess und dem Prozess pid aufgebaut. Stirbt einer der gelinkten Prozesse (auch bei Terminierung), so wird der andere ebenfalls terminiert. Dies ist zum Aufr aumen bei Prozessende praktisch. Alternativ ist es aber auch m oglich, anstelle kooperativ mit zu sterben, eine Nachricht zu empfangen, fallse der andere gelinkte Prozesse abst urtzt. ist auch der Empfang einer Benachrichtigung m oglich. Wurde mit folgendem Funktionsaufruf process_flag(trap_exit, true) ein Prozessag gel oscht, so erh alt man anstelle des Mitsterbens eine Nachricht der Form: {EXIT, reason, pid}, wobei reason den Grund f ur die Prozessterminierung und pid den abgest urzten Prozess angibt.

5.10 Systemabstraktionen
Bei der Entwicklung von Erlang-Anwendungen f allt auf, dass in verteilten Systemen immer wieder ahnlichen Prozess-Muster auftreten. Diese wurden als generische Varianten heraus gearbeitet und stehen als Bibliothek in Erlang zur Verf ugung. In der Vorlesung haben wir uns den gen_server etwas genauer angeschaut. Er eigent sich zur Entwicklung von Client-Server-Systemen. Es werden insbesondere zwei unterschiedliche Arten der Anfrage von Clienten beim Server unterst utzt: synchrone Kommunikation (wartet auf Antwort; gen_server:call) und asynchrone Kommunikation (kein Warten auf Antwort; gen_server:cast). Auerdem werden nat urlich auch die anderen Erlang-Mechanismen, wie Linking und Code-Austausch zur Laufzeit unterst utzt.

32

Der Vorteil der Verwendendung des Frameworks ist, dass bei der Implementierung des Protokolls weniger Fehler gemacht werden k onnen und aufbauend noch weitere Abstraktionen, wie z.B. der Supervisiontree, deniert werden k onnen. Als Beispiel f ur die Verwendung eines gen_servers realisieren wir nun unseren Chat in diesem Framework: Listing 29: Chat unter Verwendung des generischen Servers
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57

module ( genChat ) . b e h a v i o u r ( g e n s e r v e r ) . e x p o r t ( [ s t a r t / 0 ] ) . e x p o r t ( [ l o g i n / 2 , msg / 2 , l o g o u t / 1 , who / 1 ] ) . e x p o r t ( [ i n i t / 1 , h a n d l e c a l l / 3 , h a n d l e c a s t / 2 , h a n d l e i n f o / 2 , terminate /2 , code change / 3 ] ) . s t a r t ( ) > b a s e : putStrLn ( S e r v e r s t a r t e d ) , g e n s e r v e r : s t a r t l i n k ( { l o c a l , c h a t } , genChat , [ ] , [ ] ) . i n i t ( ) > b a s e : putStrLn ( S e r v e r i n i t i a l i z e d ) , { ok , [ ] } . % Asynchronous i n t e r f a c e h a n d l e c a s t ( { message , Text , P } , C l i e n t s ) > case l o oku p (P , C l i e n t s ) o f nothing > ok ; { j u s t , Name} > b r o a d c a s t ( { message , Text , Name } , C l i e n t s ) end , { noreply , C l i e n t s } ; h a n d l e c a s t ( { l o g o u t , P } , C l i e n t s ) > case l o oku p (P , C l i e n t s ) o f nothing > { n o r e p l y , C l i e n t s } ; { j u s t , Name} > NewClients=remove (P , C l i e n t s ) , b r o a d c a s t ( { l o g o u t , Name } , NewClients ) , { n o r e p l y , NewClients } end . % Synchronous i n t e r f a c e h a n d l e c a l l ( { l o g i n , Name } , { ClientP , } , C l i e n t s ) > b a s e : print ( C l i e n t P ) , b r o a d c a s t ( { l o g i n , Name } , C l i e n t s ) , { reply , { l o g g e d I n , s e l f ( ) , l i s t s : map( fun ( { ,N} ) > N end , C l i e n t s ) } , [ { ClientP , Name } | C l i e n t s ] } ; h a n d l e c a l l ( who , C l i e n t P , C l i e n t s ) > { reply , { o n l i n e , l i s t s : map( fun ( { ,N} ) > N end , C l i e n t s ) } , Clients }. h a n d l e i n f o ( Msg , C l i e n t s ) > b a s e : putStrLn ( Unexpected Message : ++b a s e : show ( Msg ) ) , { noreply , C l i e n t s } . t e r m i n a t e ( shutdown , C l i e n t s ) > ok . c o d e c h a n g e ( OldVsn , S t a t e , E x t r a ) > % Code t o change S t a t e { ok , S t a t e } . %C l i e n t i n t e r f a c e l o g i n ( Node , Name) > g e n s e r v e r : c a l l ( { chat , Node } , { l o g i n , Name } ) . msg ( Pid , Msg ) > g e n s e r v e r : c a s t ( Pid , { message , Msg , s e l f ( ) } ) . l o g o u t ( Pid ) > g e n s e r v e r : c a s t ( Pid , { l o g o u t , s e l f ( ) } ) .

33

58 59 60 61 62 63 64 65 66 67 68 69 70 71 72

who ( Pid ) > g e n s e r v e r : c a l l ( Pid , who ) . %h e l p e r f u n c t i o n s b r o a d c a s t ( , [ ] ) > ok ; b r o a d c a s t ( Message , [ { P , } | C l i e n t s ] ) > P ! Message , b r o a d c a s t ( Message , C l i e n t s ) . remove ( , [ ] ) > [ ] ; remove (K, [ { K, } | KVs ] ) > remove (K, KVs ) ; remove (K, [ KV | KVs ] ) > [KV | remove (K, KVs ) ] . l o o k u p ( , [ ] ) > n o t h i n g ; l o o k u p (K, [ { K,V } | ] ) > { j u s t ,V } ; l o o k u p (K, [ | KVs ] ) > lo oku p (K, KVs ) .

Ahnlich funktionieren die generischen Abstraktionen einer Finite-State-Maschine (gen_fsm)und des Event Handlings (gen_event). Die Prozesse der generische Prozessabstraktionen k onnen dann auch noch mit Hilfe der Supervisiontree-Abstraktion struturiert werden. Hier k onnen dann Strategien, wie die Anwendung auf den Ausfall einzelner Komponenten reagieren soll, deniert werden. Hierdurch wird die Entwicklung robuster Anwendungen zus atzlich unterst utzt.

6 Grundlagen der Verteilte Programmierung


Computer bilden Knoten in Netzwerken und k onnen miteinander kommunizieren. Wegen der Komplexit at der Netzwerktheorie wurden Schichten (Layer) zur Aufgabenverteilung eingef uhrt. G angigste Ansatz: Open Systems Interconnection (OSI)-Modell der International Standards Organisation (ISO)

6.1 7 Schichten des ISO-OSI-Referenzmodells


1. Physical Layer Stellt den Strom auf der Leitung bzw. die 0 und 1 dar 2. Data Link Layer Gruppierung von Bits/Bytes in Frames (Startmarken, Endmarken, Checksummen) Versand zwischen benachbarten Knoten defekte Frames werden verworfen 3. Network Layer Bildung von Datenpaketen statt Frames (Netzwerkadresse, Routing) 4. Transport Layer automatische Fehlererkennung und Fehlerkorrektur Flusskontrolle zur Begrenzung der Datenmenge ( Vermeidung von Uberlast) 5. Session Layer Anwendung-zu-Anwendung-Verbindung Kommunikationssitzung, aber auch verbindungslose Kommunikation m oglich 6. Presentation Layer Representation von Daten und ihrer Konvertierung (z. B. Kompression, Verschl usselung) 7. Application Layer Anwendungen (z. B. Java-Anwendung)

6.2 Protokolle des Internets


1. Internet Protocol (IP): bendet sich auf Schicht 3 (network layer) wird sowohl in local area networks (LANs) als auch in wide area networks (WANs) verwendet

34

Informationsaustausch zwischen Endknoten (Hosts) mit IP-Paketen oder IP-Datagrammen keine Abh angigkeit zwischen Paketen verbindungslos Versionen: IPv4 (32 Bit-Adressen), IPv6 (128 Bit-Adressen) IP-Adresse: z. B. 134.245.248.202 f ur falbala Die IP-Adresse ist f ur uns eine eindeutige Adresse im Internet. Sie wird beim Routing verwendet. Alternativ: Hostnames, Domain Name System (DNS) falbala.informatik.uni-kiel.de wobei falbala der Rechnername und informatik.uni-kiel.de die (Sub-) Domain sowie uni-kiel.de die Domain sind 2. Transmisson Control Protocol (TCP): bendet sich auf Schicht 4 (transport layer) garantiert die Zustellung (durch Wiederholung) und richtige Reihenfolge der gesendeten Bytes verwendet IP sendet Byte-Sequenz sorgt f ur faire Lastverteilung arbeitet mit Ports (in der Regel) von 0 bis 65535 h alt die Verbindung zwischen Hosts 3. User Datagram Protocol (UDP): bendet sich auf Schicht 4 (transport layer) sendet nur Datenpakete arbeitet mit Ports keine garantierte Zustellung keine garantierte richtige Reihenfolge verbindungslos schneller als TCP ist fast nur ein Wrapper f ur IP

6.3 Darstellung von IP-Adressen/Hostnames in Java


Die Klasse InetAddress im Paket java.net behandelt sowohl IPv4 als auch IPv6. Es stehen zwei Klassenmethoden zur Konstruktion zur Verf ugung: static InetAddress getByName(String hostname) throws java.net.UnknownHostException mit folgenden M oglichkeiten f ur den hostname: IP-Adresse das Objekt wird angelegt, falls die IP-Adresse bekannt ist Domain-Name DNS-Lookup; das Objekt wird angelegt, falls die IP-Adresse bekannt ist localhost das Objekt wird mit der IP-Adresse 127.0.0.1 angelegt getAllByName(String hostname) liefert alle IP-Adressen eines Hosts Zur Ausgabe stehen weiterhin folgende Methoden zur Verf ugung: String getHostAddress() liefert eine IP-Adresse des Hosts String getHostName() liefert den bei der Konstruktion angegebenen hostname boolean eqauls(InetAddress i) vergleicht zwei IP-Adressen

35

6.4 Netzwerkkommunikation
Der Rechner kommuniziert u ber sogenannte Ports (durchnummeriert von 0 bis 65535) mit dem Netzwerk. Auf Programmiersprachenseite werden die Ports u ber Sockets angesprrochen, wobei pro Port nur eine Socketverbindung m oglich ist. 6.4.1 UDP in Java Z. B. n utzlich bei DNS-Anfragen, Audio-/Videodaten, Zeitsignalen, bei zeitkritischen Anwendungen. Eigentlich nur Wrapper f ur IP-Pakete. Datagram Packet Klasse: repr asentiert UDP-Pakete Verwendung beim Senden und Empfangen unterschiedliche Bedeutung der Adresse: Senden Zieladresse Empfangen Sourceadresse (g unstig f ur Antwort) Aufbau: siehe Abbildung 6 Konstruktoren: DatagramPacket(byte[] buffer, int length) zum Empfang von Paketen DatagramPacket(byte[] buffer, int length, InetAdress dest_addr, int dest_port) zum Senden Methoden zum Modizieren: getAddress, setAddress, getData, setData, getLength, setLength, getPort, setPort zur Anbindung an Ports: Datagram Socket Klasse: zum Senden und Empfangen von Datagram Paketen Aufbau siehe Abbildung 7 Konstruktoren: DatagramSocket() throws SocketException nur zum Senden von UDP-Paketen (freier Port wird verwendet und belegt); eine Exception wird geworfen, falls kein freier Port mehr vorhanden ist DatagramSocket(int port) throws SocketException zum Senden und Empfangen geeignet (ist aber eher f ur Server gedacht); eine Exception wird geworfen, falls der Port belegt ist wichtigste Methoden: close() Schlieen des Sockets und Freigeben des Ports getLocalPort, setLocalPort getLocalAddress getReceiveBuffersize, setReceiveBuffersize Abfragen bzw. Setzen der maximalen Gr oe des Puers void receive(DatagrammPaket p) throws IOException liest UDP-Paket (gepuert) und schreibt dieses in p 1. IP- und PortAdresse werden mit Senderadresse und Senderport u berschrieben 2. das length-Attribut enth alt die tats achliche L ange, die kleiner gleich der Gr oe des Pakets ist 3. blockiert, bis das Paket empfangen wurde Programmierung mit UDP verwendete Klassen: DatagramPacket mit receive und send das, was u bermittelt werden soll DatagramSocket

36

Abbildung 6:

Abbildung 7: F ur receive gibt es auch einen Timeout, welcher mit int getSoTimeout() gelesen und mit setSoTimeout(int t) gesetzt werden kann. Die Zeit wird hierbei in Millisekunden angegeben und deniert, wie lange receive maximal blockiert; falls keine Nachricht in dieser Zeit eintrit, so gibt es eine java.io.InterruptedIOException. Die M oglichen Verbindungen k onnen auch eingeschr ankt werden: connect(InetAddress remoteAddr, int remotePort) es sind nur noch Verbindungen mit dem angegebenen Rechner m oglich (Filter f ur ein- und ausgehende Pakete) disconnect getInetAddr getPort Statt byte[] k onnen auch Streams angebunden werden. Als Beispiel betrachten wir eine einfache Client-/Server-Architektur, die einen Inkrementserver zur Verf ugung stellt. Der Client schickt einen WErt hin und erh alt den inkrementierten Wert zur uck.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21

import j a v a . n e t . ; p u b l i c c l a s s Sender { p u b l i c s t a t i c v o i d main ( S t r i n g [ ] a r g s ) { by te [ ] b = new byte [ 1 ] ; b [ 0 ] = Byte . p a r s e B y t e ( a r g s [ 0 ] ) ; DatagramPacket p a c k e t = new DatagramPacket ( b , 1 ) ; try { p a c k e t . s e t A d d e s s ( I n e t A d d r e s s . getByName ( mickey . i n f o r m a t i k . uni k i e l . de ) ) ; packet . setPort ( 6 0 0 0 1 ) ; DatagramSocket s o c k e t = new DatagramSocket ( ) ; s o c k e t . send ( p a c k e t ) ; socket . r e c e i v e ( packet ) ; System . out . p r i n t l n ( R e s u l t : +b [ 0 ] ) ; } c a t c h ( E x c e p t i o n e ) { System . out . p r i n t l n ( Bad I n t e r n e t c o n n e c t i o n ) ; } } } public c l a s s Receiver { p u b l i c s t a t i c v o i d main ( S t r i n g [ ] a r g s ) {

37

22 23 24 25 26 27 28 29 30 31 32 33 34 35

by te [ ] b = new byte [ 1 ] ; b [ 0 ] = 42; try { DatagramPacket p a c k e t = new DatagramPacket ( b , 1 ) ; DatagramSocket s o c k e t = new DatagramSocket ( 6 0 0 0 1 ) ; while ( b [ 0 ] ! = 0 ) { socket . r e c e i v e ( packet ) ; System . out . p r i n t l n ( R e c e i v e d : +b [ 0 ] ) ; b[0]++; s o c k e t . send ( p a c k e t ) ; } } c a t c h ( E x c e p t i o n e ) { System . out . p r i n t l n ( Bad I n t e r n e t c o n n e c t i o n ) ; } } }

Bemerkungen: Zeile 5: Byte-Array mit einem Eintrag Zeile 6: parseByte(...) u bersetzt das Hauptargument als Byte (-128 bis 127) Zeile 7: das erste Argument ist das Byte-Array; das zweite Argument die Packetl ange Zeile 13: Anfrage an den Server Zeile 14: Abwarten der Antwort (blockiert bis irgendein Packet ankommt) Zeile 23: der Inhalt ist hier egal, da als erstes etwas Empfangen und der Inhalt dabei u berschrieben wird Zeile 26: dient der Denition des zu h orenden Ports (Server-Port des Dienstes) Zeile 27: Beenden des Receivers u oglich ber java Sender -1 m Zeile 28: blockiert, bis eine Anfrage reinkommt Zeile 30: Inkrement-Service Zeile 31: Absenden der Antwort (Adresse und Port sind durch den Paketeingang bereits deniert; Verwendung der Adresse des Senders f ur die Antwort) Problem: falls der Sender keine Antwort bekommt, so blockiert dieser dauerhaft eine L osungsm oglichkeit w are ein Timeout ggf. mit erneuter Anfrage (unsichere Kommunikation). Transmission Control Protocol (TCP) Eigenschaften: verbindungsorientiertes Protokoll sichere Ubertragung ist garantiert (es gehen keine Pakete verloren) Versenden von Datenstr omen werden automatisch in TCP-Pakete aufgebrochen richtige Reihenfolge ist garantiert die Bandbreite wird zwischen mehreren TCP-Verbindungen fair geteilt TCP unterscheidet zwischen Client und Server einer Verbindung der TCP-Client initiiert eine Verbindung der TCP-Server antwortet auf diese Verbindung bidirektionale Verbindung ist etabliert TCP-Sockets in java.net verbinden zwei Ports auf (eventuell) unterschiedlichen Rechnern (kann auch f ur UDP genutzt werden) Konstruktoren: Socket(InetAddr addr, int port) thorws java.net.IOException stellt Verbindung zu addr und port her, wobei als lokaler Port dieser Verbindung ein freier Port verwendet wird Socket(InetAddr addr, int port, InetAddr localAddr, int localPort) throws java.net.IOException analog wie oben, aber mit festem lokalem Port ( besser nicht verwenden, da ein Fehler auftritt, falls Port besetzt ist)

38

Socket(String host, int port) throws java.net.IOException Beachte der Socket blockiert, bis die Verbindung aufgebaut ist. Methoden: close() schliet die Verbindung (die Daten sollten vorher geushed werden, d. h. der Puer sollte geleert und die Daten u bertragen werden) InetAddr getInetAddress() liefert remoteAddress int getPort() liefert den remotePort getLocalInetAddress getLocalPort InputStream getInputStream() OutputStream getOutputStream() es ist auch eine Konguration der Socket-Verbindung (z. B. Timeout, etc.) m oglich Lesen und Schreiben geschieht u ber die Streams. Listing 30: Im Beispiel verbindet sich der Client mit dem Server und empf angt eine Nachricht
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

import j a v a . n e t . ; import j a v a . i o . ; p u b l i c c l a s s DaytimeClient { p u b l i c s t a t i c v o i d main ( S t r i n g [ ] a r g s ) { try { S o c k e t s o c k e t = new S o c k e t ( mickey , 6 0 0 0 1 ) ; System . out . p r i n t l n ( Connection e s t a b l i s h e d ) ; B u f f e r e d R e a d e r r e a d e r = new B u f f e r e d R e a d e r ( new InputStreamReader ( s o c k e t . g e t I n p u t S t r e a m ( ) ) ); System . out . p r i n t l n ( R e s u l t : +r e a d e r . r e a d L i n e ( ) ) ; socket . close ( ) ; } c a t c h ( IOException i o e ) { System . out . p r i n t l n ( E r r o r : +i o e ) ; } } }

Bemerkungen: Zeile 12: Byte-Array mit einem Eintrag TCP-Server: Klasse ServerSocket Konstruktoren: ServerSocket(int port) throws java.io.IOException horcht auf dem Port ServerSocket(int port, int NumberOfClients) throws java.io.IOException, wobei numberOfClients die Gr oe der Backlog-Queue (Anzahl der wartenden Clients; default: 50) darstellt Zum Akzeptieren von Verbindungen: Socket accept() throws java.io.IOException wartet auf den Client und akzeptiert diesen Verbindung close() schliet den ServerSocket getSoTimeout() setSoTimeout(int ms) Setzen eines Timeout f ur das accept Listing 31: Server des Beispiels
1 2 3 4 5 6 7

p u b l i c c l a s s DayTimeServer { p u b l i c s t a t i c v o i d main ( S t r i n g [ ] a r g s ) { try { S e r v e r S o c k e t s e r v e r = new S e r v e r S o c k e t ( 6 0 0 0 1 ) ; while ( true ) { Socket nextClient = s e r v e r . accept ( ) ; OutputStream out = n e x t C l i e n t . getOutputStream ( ) ;

39

8 9 10 11 12 13 14 15 16 17

new P r i n t S t r e a m ( out ) . print ( new j a v a . u t i l . Date ( ) ) ; out . c l o s e ( ) ; nextClient . close ( ) ; // Ende d e r S o c k e t Verbindung } } c a t c h ( BindException be ) { System . out . p r i n t l n ( S e r v i c e a l r e a d y r u n n i n g on p o r t 60001 ) ; } c a t c h ( IOException i o e ) { System . out . p r i n t l n ( I /O e r r o r : +i o e ) ; } } }

6.5 Socket-Optionen
Zugri/Einstellung u ber Socket-Methoden, d. h. Methoden der Klasse Socket SO_KEEPALIVE: setKeepAlive(true) regelm aiges Pollen (Anfragen) zum Verbindungstest SO_RCVBUF: setReceiveBuffersize(size) Betriebssystempuer wird in seiner Gr oe beeinusst (size ist in Byte) SO_SNDBUF: setSendBuffersize(size) Betriebssystempuer wird in seiner Gr oe beeinusst (size ist in Byte) SO_LINGER: setSoLinger(true,50) auch beim Schlieen des sockets wird noch f ur 50 Millisekunden versucht, die Daten zu senden TCP_NODELAY Ver anderung des TCP-Protokolls, so dass keine Transmissionskontrolle (Warten, bis ein Paket voll ist, bevor es gesendet wird) mehr stattndet ACHTUNG: die Performance kann sinken SO_TIMEOUT: setSoTimeout(int time) Leseoperation versucht time Millisekunden zu lesen. Eventuell tritt eine InterruptedIOException auf, falls keine Antwort kommt (time=0 entspricht keinem Timeout). Listing 32: Beispiel
1 2 3 4 5 6 7

try { s . setSoTimeout ( 2 0 0 0 ) ; . . . l e s e aus S o c k e t s . . . } catch ( InterruptedIOException i oe ) { System . e r r . p r i n t l n ( S e r v e r a n t w o r t e t n i c h t ! ) ; } c a t c h ( IOException i o ) { . . . }

6.6 Sockets in Erlang


In der Vorlesung haben wir die TCP-Kommunikation u ber Sockets in Erlang kennen gelernt. Die Prinzipien sind nat urlich identisch, mit denen in Java. Als Beispielanwendung haben wir einen einfachen Tupelserver entwickelt, welchen wir sp ater auch als Registry verwenden k onnen: Listing 33: TCP-Version eines Key-Value-Servers in Erlang
1 2 3 4 5 6 7 8 9 10 11 12 13

module ( t c p ) . e x p o r t ( [ s e r v e r / 0 , c l i e n t / 0 , r e q u e s t H a n d l e r / 3 ] ) . s e r v e r ( ) > { ok , LSock } = g e n t c p : l i s t e n ( 5 0 4 2 , [ l i s t , { packet , l i n e } , { r e u s e a d d r , true } ] ) , DB = spawn ( k e y V a l u e S t o r e , s t a r t , [ ] ) , acceptLoop ( LSock ,DB) . acceptLoop ( LSock ,DB) > spawn ( tcp , r e q u e s t H a n d l e r , [ LSock ,DB, s e l f ( ) ] ) , receive next > acceptLoop ( LSock ,DB)

40

14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49

end . r e q u e s t H a n d l e r ( LSock ,DB, Parent ) > { ok , Sock } = g e n t c p : a c c e p t ( LSock ) , Parent ! next , receive { tcp , Sock , S t r } > case l i s t s : s p l i t w i t h ( fun (C) > C/=44 end , l i s t s : delete ( 1 0 , S t r ) ) o f { s t o r e , [ | Arg ] } > { Key , [ | Value ] } = l i s t s : s p l i t w i t h ( fun (C) > C/=44 end , Arg ) , DB! { s t o r e , Key , Value } ; { loo kup , [ | Key ] } > DB! { lookup , Key , s e l f ( ) } , receive Ans > g e n t c p : send ( Sock , b a s e : show ( Ans)++ \ n ) end end , g e n t c p : c l o s e ( Sock ) ; Other > b a s e : print ( Other ) end .

c l i e n t ( ) > case b a s e : getLn ( ( s ) t o r e , ( l ) ookup : ) o f s > Key = b a s e : getLn ( Key : ) , Value = b a s e : getLn ( Val : ) , { ok , Sock } = g e n t c p : c o n n e c t ( l o c a l h o s t , 5 0 4 2 , [ l i s t , { packet , l i n e } ] ) , g e n t c p : send ( Sock , s t o r e , ++Key++ , ++Value++ \ n ) , g e n t c p : c l o s e ( Sock ) ; l > Key = b a s e : getLn ( Key : ) , { ok , Sock } = g e n t c p : c o n n e c t ( l o c a l h o s t , 5 0 4 2 , [ l i s t , { packet , l i n e } , { active , false }]) , g e n t c p : send ( Sock , lookup , ++Key++ \ n ) , { ok , Res } = g e n t c p : r e c v ( Sock , 0 ) , b a s e : putStrLn ( Res ) , g e n t c p : c l o s e ( Sock ) end .

6.7 Verteilung von Kommunikationsabstraktionen


Nachdem wir nun verstanden haben, wie eine verteilte Kommunikation u onnen ber TCP funktioniert, k wir uns u berlegen, wie Kommunikationsabstraktionsabstraktionen transparent in eine verteiltes System eingebettet werden k onnen. Hierbei ist es egal, ob es sich um Pids in Erlang, RemoteObjekte bei RMI oder ahnliches handelt. Man ben otigt eine externe Darstellung des entsprechenden Objekts im Netzwerk, welche u ber IP-Adresse des Rechners, einen Port und ggf. noch eine weitere Identikationsnummer, einen Zugri auf das entsprechende Objekt von Remote erm oglicht. Wird ein entsprechendes (lokales) Objekt serialisiert, wandelt man es in seine Remote-String-Darstellung um und verschickt diese. Alle Kommunikation auf ein Remote-Objekt wird dann u ber TCP serialisiert, wodurch entsprechend auch wieder andere Kommunikationsabstraktionen verteilt werden k onnen. Als Beispiel hierf ur haben wir in Vorlesung und Ubung Channels in Java, Erlang bzw. Haskell f ur einen Zugri von Remote erweitert. Hierbei haben wir insbesondere die folgenden Aspekte diskutiert und realisiert: Verwendung des Key-Value-Stores als Registry Schreiben eines Channels von Remote Optimierung der Kommunikation durch Zusammenlegen der Kommunikation auf einen Port Garbage Collection von nicht mehr verwendeten Channels Lesen aus einem remote Channel Realisierung von Linking

41

7 Synchronisation durch Tupelr aume


Ansatz ohne Kommunikation zwischen zwei Partnern (Server/Client) Linda: Kommunikation durch Bekanntmachen und Abfragen von Tupeln. Das grudlegende Modell sieht wie folgt aus: zentraler Speicher (ggf. auch mehrere) Tupelraum als Multimenge von Tupeln viele unabh angige Prozesse, die Tupel in den Tupelraum hinzuf ugen oder Tupel aus diesem lesen (entfernen) k onnen sprachunabh angig: Linda ist ein Kommunikationsmodell, das zu verschiedenen Sprachen hinzugef ugt werden kann (z. B. C-Linda, Fortran-Linda, Scheme-Linda, Java-Spaces) Hierbei basiert Linda auf folgenden Grundoperationen: out(t) f ugt das Tupel t in den Tupelraum ein. Berechnungen in t werden vor dem Einf ugen ausgewertet. Beispiel: out(("hallo",1+3,6.0/4.0)) f ugt das Tupel ("hallo",4,1.5) in den Tupelraum ein. in(t) entfernt das Tupel t aus dem Tupelraum, falls t dort vorhanden ist. Ansonsten suspendiert es, bis t im Tupelraum eingef ugt wurde. t kann auch Variablen enthalten (Pattern Matching). Diese parsen immer und werden an entsprechende Werte aus dem Tupelraum gebunden. Beispiel: in(("hallo",?i,?f)) entfernt obig eingef ugtes Tupel und bindet i/4 und f/1.5 rd(t) wie in(t), aber das Tupel wird nicht entfernt eval(stmt) erzeugt einen neuen Prozess, der stmt auswertet inp(t), rdp(t) wie in, rd, aber ohne Blockade, falls das Tupel nicht vorhanden ist. Als R uckgabewert erh alt man einen boolschen Wert. Dieser ist false, falls t nicht im Tupelraum vorhanden ist. Beispiel: zwei Prozesse, die abwechselnd aktiv sind (ping-pong) in Linda
1 2 3 4 5 6

ping ( ) { while ( true ) { out ( p i n g ) ; i n ( pong ) ; } } pong ( ) { while ( true ) { in ( ping ) ; out ( pong ) ; } }

1 2 3 4 5 6

Listing 34: Hauptprogramm


1 2 3 4

main ( ) { eval ( p i n g ( ) ) ; eval ( pong ( ) ) ; }

Es ist aber auch m oglich, Datenstrukturen durch Tupel darzustellen: Interpretiere bestimmte Werte als Referenzen im Speicher (Tupelraum), z. B. durch Integer-Werte. Dann ist z. B. ein Array kodierbar als: {("A",1,r1),("A",2,r2),...,("A",n,rn);...} Der Zugri auf einzelne Arraykomponenten kann dann z. B. durch die Multiplikation der 4. Komponente mit 3 erfolgen. in("A",4,?e); out("A",4,3*e);

42

Anwendung: verteilte und parallele Bearbeitung komplexer Datenstrukturen Listing 35: Rechenoperation auf jedem Element
1 2 3 4 5

f o r ( i =1; i <=n ; i ++) { eval ( f ( i ) ) ; } f ( int i ) { i n ( A , i , ? r ) ; out ( A , i , compute ( r ) ) ; }

Dies ist nur sinnvoll, wenn compute aufwendig ist und die Verteilung auf mehrere Prozessoren m oglich ist. Formulierung von Synchronisationsproblemen: Kodierung der dinierenden Philosophen
1 2 3 4 5 6 7 8 9 10

phil ( int i ) { while ( true ) { think ( ) ; in ( stab , i ) ; i n ( s t a b , ( i +1)% j ) ; eat ( ) ; out ( s t a b , i ) ; out ( s t a b , ( i +1)% j ) ; } }

Listing 36: Initialisierung


1 2 3 4

f o r ( i =0; i <s ; i ++) { out ( s t a b , i ) ; eval ( p h i l ( i ) ) ; }

Eine Client/Server-Kommunikation ist auch modellierbar Idee: Zur Zuordnung der Antworten zu Anfragen werden von dem Server eindeutige IDs verwendet:
1 2 3 4 5 6 7 8 9 10

server () { i n t i =1; eval ( i d s e r v e r ( 1 ) ) ; while ( true ) { in ( request , i , ? req ) ; ... out ( r e s p o n s e , i , r e s ) ; i ++; } } idserver ( int i ) { while ( true ) { i n ( newID ) ; out ( newID , i ) ; i ++; } } Client () { int id ; out ( newID ) ; i n ( newID , ? i d ) ; out ( r e q u e s t , ? id , 1 ) ; i n ( r e s p o n s e , id , ? r e s 1 ) ;

1 2 3 4 5 6 7

1 2 3 4 5 6

43

7.1 Java-Spaces
Linda in Java out t space.write(Entry e, Transaction tr, long lease) mit: Entry e als Interface f ur Eintr age in den Tupelspace Transaction tr als Transaktionskonzept zur atomaren Ausf uhrung mehrerer Modelle von Tupelr aumen, sonst null long lease gibt die Zeit an, wie lange ein Tupelraum verbleibt in t space.take(Entry e, Transaction tr, long lease) mit: Entry e als Template (Pattern) long lease als maximale Suspensionszeit das Template kann auch Variablen/Wildcards in Form von null enthalten. Sas Template matcht den Eintrag im Space, falls: 1. das Template den gleichen Typ oder Obertyp wie der Eintrag hat 2. alle Felder des Templates matchen: Wildcard (null) matcht immer Werte matchen nur gleiche Werte Das Matchen der Variablen erfolgt durch das Ersetzen von null durch den korrekten Wert. rd(t) space.read(...) eval(stmt) Java Threads inp(t), rdp(t) u ber lease=0 Beachte: Die Objektidentit at bleibt in Space nicht erhalten. Objekte werden (de-)serialisiert, da es sich h aug um verteilte Anwendungen handelt. Spaces k onnen lokal oder im Netzwerk gestartet werden. Sie k onnen persistent sein. Sie k onnen u ber Jini-lookup-Sevice oder RMI-Registrierung oder andere gestartet werden.

7.2 Implementierung von Tupelr aumen in Erlang


Um zu verstehen, wie Tupelr aume realisiert werden k onnen, betrachten wir eine prototypische Implementierung in Erlang. Erlang verf ugt zwar auch u ber Pattern Matching, Pattern sind aber keine B urger erster Klasse und k onnen nicht als Nachrichten verschickt werden. Es ist in Erlang aber m oglich, Funktionen zu verschicken. Somit k onnen wir recht einfach partielle Funktionen verschicken, welche das Pattern Matching im Tupelraum realisieren. Als Beispiel k onnen wir das Pattern {hallo,X,Y} durch die Funktionsdenition
1

fun ( { h a l l o , X,Y) > {X,Y} end

realisieren. Hierbei liefern wir die Bindungen als Parr der gebundenen Variablen zur uck. Die Partialit at der Funktion verwenden wir um das Nicht-Matchen des Patterns auszudr ucken. Im Server, k onnen wir diesen Fall mit Hilfe eines catch-Konstrukts erkennen und entsprechend verfahren. Im Tupel-Server ben otigen wir zwei Strukturen (Listen), zur Speicherung von Anfragen. In der einen Liste werden alle Werte im Tupelraum gespeichert. Neue in- bzw. rd-Requests werden zun achst gegen alle Werte gemacht und ggf. direkt beantwortet. Sollte kein passender Wert gefunden werden, speichern wir die Requests in einer zweiten Liste. Wird sp ater dann ein neuer Wert eingef ugt, wird dieser entsprechend f ur alle oenen Requests u uft und ggf. eine Antwort zur uck geschickt. berpr Das konkrete Implementierung ergibt sich wie folgt: Listing 37: Erlang Implementierung des Linda-Modells
1 2 3 4 5 6 7 8

module ( l i n d a ) . e x p o r t ( [ s t a r t / 0 , out / 2 , i n / 2 , rd / 2 ] ) . s t a r t ( ) > r e g i s t e r ( l i n d a , s e l f ( ) ) , lindaLoop ( [ ] , [ ] ) . l i n d a L o o p ( Ts , Reqs ) > receive

44

9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58

{ out , T} > { Reqs1 , KeepT } = findMatchingReq (T, Reqs ) , case KeepT o f true > l i n d a L o o p ( [ T | Ts ] , Reqs1 ) ; f a l s e > l i n d a L o o p ( Ts , Reqs1 ) end ; { InRd , F , P} > case f i n d M a t c h i n g T u p l e (F , Ts , InRd ) o f nothing > l i n d a L o o p ( Ts , [ { F , P , InRd } | Reqs ] ) ; { j u s t , { Match , Ts1 }} > P ! { tupleMatch , Match } , l i n d a L o o p ( Ts1 , Reqs ) end end . findMatchingReq ( , [ ] ) > { [ ] , true } ; findMatchingReq (T , [ Req | Reqs ] ) > { F , P , InRd } = Req , case c a t c h F(T) o f { EXIT , } > { Reqs1 , KeepT } = findMatchingReq (T, Reqs ) , { [ Req | Reqs1 ] , KeepT } ; Match > P ! { tupleMatch , Match } , case InRd o f i n > { Reqs , f a l s e } ; rd > findMatchingReq (T, Reqs ) end end . f i n d M a t c h i n g T u p l e ( , [ ] , ) > n o t h i n g ; f i n d M a t c h i n g T u p l e (F , [ T | Ts ] , InRd ) > case c a t c h F(T) o f { EXIT , } > case f i n d M a t c h i n g T u p l e (F , Ts , InRd ) o f nothing > n o t h i n g ; { j u s t , { M1, Ts1 }} > { j u s t , { M1 , [ T | Ts1 ] } } end ; Match > case InRd o f i n > { j u s t , { Match , Ts } } ; rd > { j u s t , { Match , [ T | Ts ] } } end end . out ( Space , T) > Space ! { out , T } . i n ( Space , F) > Space ! { in , F , s e l f ( ) } , receive { tupleMatch , Match } > Match end . rd ( Space , F) > Space ! { rd , F , s e l f ( ) } , receive { tupleMatch , Match } > Match end .

8 Spezikation und Testen von Systemeigenschaften


Das Testen von Systemeigentschaften von nebenl augen Systemen geht u atze, wie Unit-Tests ber Ans oder Zusicherungen im Quellcode hinaus. Man ist in der Regel daran interessiert, globale Systemeigenschaften zu analysieren, die sich aus den Zust anden der einzelnen Prozesse zusammen setzen. Als Beispiel k onnen wir noch einmal auf das nebenl auge Ver andern eines geteilten Zustands zur uckkommen, hier als Client-Server-Struktur in Erlang: Listing 38: Kritischer Bereich
1 2

module ( c r i t i c a l ) . e x p o r t ( [ s t a r t / 0 ] ) .

45

3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23

s t a r t ( ) > S = spawn ( fun ( ) > s t o r e ( 4 2 ) end ) , spawn ( fun ( ) > i n c ( S ) end ) , dec ( S ) . s t o r e (V) > r e c e i v e { lookup , P} > P ! V, s t o r e (V ) ; { s e t , V1 } > s t o r e (V1) end . i n c ( S ) > S ! { lookup , s e l f ( ) } , receive V > S ! { s e t ,V+1} end , inc (S ) . dec ( S ) > S ! { lookup , s e l f ( ) } , receive V > S ! { s e t , V1} end , dec ( S ) .

Hierbei stellen die Zust ande, bevor der Zustand neu geschireben wird sicherlich kritische Zust ande da. Eigentlich sollten beide Client-Prozesse nie gleichzeitig in diesen Zust anden sein, da dann der Speicher in Abh angigkeit eines veralteten Wertes ver andert wird. Solche Eigenschaften k onnen elegant mit Hilfe von Temporallogiken ausgedr uckt werden.

9 Linear Time Logic (LTL)


Die Lineare temporalale Logik (LTL) erm oglicht es Eigenschaften von Pfaden eines Systems auszudr ucken. Ihre Syntax ist wie folgt deniert: Syntax der Linear Time Logic (LTL) Sei Props eine (endliche) Menge von Zustandspropositionen. Die Menge der LTL-Formeln ist deniert als kleinste Menge mit: Props LT L , LT L = LT L LT L X LT L U LT L Zustandspropositionen Negation Konjunktion Im n achsten Zustand gilt gilt bis gilt

Eine LTL-Formel wird bzgl eines Pfades interpretiert. Propositionen gelten, wenn der erste Zustand des Pfades die Propositionen erf ullt. Der Modaloperator Next X ist erf ullt, wenn auf dem Pfad ab dem n achsten Zustand gilt. Auerdem enth alt LTL den Until-Operator, welcher erf ullt ist, wenn solange gilt, bis gilt. Hierbei mus psi aber tats achlich irgendwann gelten. Formal ist die Semantik wie folgt deniert: Pfadsemantik von LTL Ein unenedliches Wort u ber Propositionsmengen = p0 p1 p2 . . . P (Props) heit Pfad. Ein Pfad erf ullt eine LTL-Formel ( |= ) wie folgt: p0 p0 p0 p1 . . . |= |= |= |= |= P X U i i i i i P p0 |= |= and |= |= i 0 : pi pi+1 . . . |= and j < i : pj pj +1 . . . |=

46

Ausgehend vom Kern von LTL k onnen wir praktische Abk urzungen defnieren, welche es h aug einfacher machen, Eigentschaften zu spezizieren. Abk urzungen in LTL true f alse F G F G W R := := := := := := := := := := P P 5 true ( ) true U F GF F G ( U ) (G ) ( U ) der boolesche Wert true der boolesche Wert false Disjunktion Implikation Irgendwann (Finally) gilt Immer (Globaly) gilt Unendlich oft gilt Nur endlich oft gilt nicht Schwaches Until (weak until) l at frei (release)

Das Release ist hierbei sicherlich nicht ganz so verst andlich, wird aber bei der Implementierung recht n utzlich sein. Ausgehend von der Pfadsemantik, erf ullt ein System eine LTL-Formel, wenn jeder Pfad des Systems die LTL-Formle erf ullt. Formal werden Systeme hierbei als Kripke-Strukturen repr asentiert: Kripke Struktur K = (S, Props, , , s0 ), wobei S eine Menge von Zust anden, Props eine Menge von Propositionen, S S die Transitionsrelation, : S P (Props) eine Beschriftungsfunktion f ur die Zust ande und s0 S der Startzustand sind, heit Kripke Struktur. Anstatt (s, s ) schreibt man in der Regel s s . Ein Zustandspfad von K ist ein unendliches Wort s0 s1 . . . S mit si si+1 und s0 der Startzustand von K. Wenn s0 s1 . . . ein Zustandspfad von K und pi = (si ) f ur alle i 0, dann ist das unendliche Wort p0 p1 . . . P (Props) ein Pfad von K.

Kripke Struktur Semantik von LTL Sei K = (S, , , s0 ) eine Kripke Struktur. K erf ullt eine LTL-Formel (K |= ) genau dann, wenn f ur alle Pfade von K: |= . Mit Hilfe von LTL k onnen im wesentlichen drei unterschiedliche Arten von Eigenschaften deniert werden: Sicherheitseigenschaften (Safety): Solche Eigenschaften haben in der Regel die Form: G Ein m ogliches Beispiel f ur k onnen f ur zwei Prozesse, f ur welche der wechselseitige Ausschluss gew ahrleistet sein soll cs1 cs2 sein, wenn der Prozess i {1, 2} seinen kritischen Bereich durch die Proposition csi anzeigt. Lebendigkeitseigenschaften (Liveness): Diese Eigenschaften haben in der Regel die Form: F , wobei sie oft auch nur bedingt gelten sollen, z.B. in einer Implikation. Ein Beispiel ist, die Antwort (answ) auf einen Request (req ) eines Servers: G (req F answ). Hierbei ist die eigentlich Lebendigkeitseigenschaft nur erw unscht, wenn eine Anfrage erfolgte. Mit Hilfe von G fordern wir, dass diese Eigenschaft u berall im System gelten soll. Fairness: Fairness kann als F beschrieben werden. Hiermit k onnen wir ausdr ucken, dass jeder beteiligte

47

Prozess unendlich oft dran kommt und vom Scheduler nicht ausgeschlossen wird. Man verwendet Fairnesseingenschaften in der Regel als Vorbedingung einer Implikation, um unfaire Pfade nicht zu betrachten. Dies entspricht der Beschr ankung auf pr aemptive Scheduler, welche wir einleitend in Betracht gezogen haben.

9.1 Implementierung von LTL zum Testen


LTL wurde urspr unglich als Spezikationslogik entwickelt, welche dann mit Hilfe von Model Checking gegen uber einer endlichen Kripkestruktur (Systembeschreibung) u uft werden kann. Das Model berpr Checking ist aber f ur reale Systeme im allgemeinen unentscheidbar, wie folgendes Erlang Beispiel zeigt: Listing 39: Unentscheidbarkeit von LTL
1

s t a r t ( ) > f ( ) , prop ( t e r m i n a t e d ) .

HIerbei soll prop(terminated) bedeuten, dass das Programm nachdem der Funktionsaufruf f() beendet wurde, in einen Zustand u uhrt wird, in dem die Proposition terminated gilt. Will man f ur berf dieses Programm die LTL-Formel F terminated entscheiden, so m usste man ja entscheiden, ob die Funktion f() terminiert. Da Erlang ja eine universelle Programmiersprache ist, ist dies aber direkt ein Widerspruch zum Halteproblem und somit im allgemeinen unentscheidbar. Auch wenn die formale Verikation also nicht m oglich ist, kann es aber dennoch sinnvoll sein, LTLEigenschaften zu testen. In wie weit die sich ergebenden Aussagen zu interpretieren sind, werden wir sp ater noch besprechen. Zun achst wollen wir den Ansatz zum LTL-Testen implementieren. Als ersten Schritt m ussen wir uns eine geeignete Darstellung von LTL-Formeln in Erlang u berlegen. Wir besch anken und hier auf einen Teil der LTL-Operatoren. Die restlichen Operatoren sollen als Ubung erg anzt werden. Bei der Negation des Untils ist die Verwendung des Release hilfreich, da dies gerade als Negation des Until deniert ist. Listing 40: LTL-Implementierung in Erlang
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

module ( l t l ) . e x p o r t ( [ prop / 1 , d i s j / 2 , c o n j / 2 , neg / 1 , x / 1 , f / 1 , g / 1 ] ) . prop ( Phi ) > { prop , Phi } . neg ( Phi ) > { neg , Phi } . d i s j ( Phi , P s i ) > { d i s j , Phi , P s i } . c o n j ( Phi , P s i ) > { c o n j , Phi , P s i } . x ( Phi ) > { x , Phi } . g ( Phi ) > { g , Phi } . f ( Phi ) > { f , Phi } . showLTL ( { prop , P } ) > b a s e : show (P ) ; showLTL ( { d i s j , Phi , P s i } ) > ( ++showLTL ( Phi)++ o r ++showLTL ( P s i)++ ) ; showLTL ( { c o n j , Phi , P s i } ) > ( ++showLTL ( Phi)++and++showLTL ( P s i)++ ) ; showLTL ( { neg , Phi } ) > ( neg ++showLTL ( Phi)++ ) ; showLTL ( { x , Phi } ) > X++showLTL ( Phi ) ; showLTL ( { f , Phi } ) > F++showLTL ( Phi ) ; showLTL ( { g , Phi } ) > G++showLTL ( Phi ) ; showLTL ( Phi ) > b a s e : show ( Phi ) .

Das erste Problem bei der Implementierung stellt die Negation dar. Diese ist u ber die Negation auf der mathematischen Meta-Ebene deniert und so nur schwer zu implementieren. Es ist aber m oglich, LTL-Formeln so aquivalent umzuformen, dass die Negation nur noch direkt vor Propositionen auftritt. Die Negation wird vom Prinzip nach Innen geschoben. Listing 41: LTL-Implementierung in Erlang
1 2

n o r m a l i z e ( true ) > true ; n o r m a l i z e ( f a l s e ) > f a l s e ;

48

3 4 5 6 7 8 9 10 11 12 13 14 15 16

n o r m a l i z e ( { prop , P} ) > prop (P ) ; n o r m a l i z e ( { d i s j , Phi , P s i } ) > d i s j ( n o r m a l i z e ( Phi ) , n o r m a l i z e ( P s i ) ) ; n o r m a l i z e ( { c o n j , Phi , P s i } ) > c o n j ( n o r m a l i z e ( Phi ) , n o r m a l i z e ( P s i ) ) ; n o r m a l i z e ( { x , Phi } ) > x ( n o r m a l i z e ( Phi ) ) ; n o r m a l i z e ( { f , Phi } ) > f ( n o r m a l i z e ( Phi ) ) ; n o r m a l i z e ( { g , Phi } ) > g ( n o r m a l i z e ( Phi ) ) ; n o r m a l i z e ( { neg , true } ) > f a l s e ; n o r m a l i z e ( { neg , f a l s e } ) > true ; n o r m a l i z e ( { neg , { prop , P}} ) > neg ( prop (P ) ) ; n o r m a l i z e ( { neg , { d i s j , Phi , P s i }} ) > c o n j ( n o r m a l i z e ( neg ( Phi ) ) , n o r m a l i z e ( neg ( P s i ) ) ) ; n o r m a l i z e ( { neg , { c o n j , Phi , P s i }} ) > d i s j ( n o r m a l i z e ( neg ( Phi ) ) , n o r m a l i z e ( neg ( P s i ) ) ) ; n o r m a l i z e ( { neg , { x , Phi }} ) > x ( n o r m a l i z e ( neg ( Phi ) ) ) ; n o r m a l i z e ( { neg , { f , Phi }} ) > g ( n o r m a l i z e ( neg ( Phi ) ) ) ; n o r m a l i z e ( { neg , { g , Phi }} ) > f ( n o r m a l i z e ( neg ( Phi } ) ) .

Als n achstes stellen wir eine Funktion zur Verf ugung, welche anhand der g ultigen Propositionen Formeln auf ihre G ultigkeit im altuellen Zustand u uft. M ogliche Ergebnisse sind hierbei true, f alse berpr und noch nicht entschieden. In letzteren Fall erhalten wir eine Formel, welche wir auf dem weiteren Pfad u ufen m ussen. berpr Listing 42: LTL-Implementierung in Erlang
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30

c h e ck ( true , P r o p s ) > true ; c h e ck ( f a l s e , P r o p s ) > f a l s e ; c h e ck ( { prop , P} , Props ) > l i s t s : member (P , Props ) ; c h e ck ( { neg , { prop , P}} , Props ) > not ( check ( prop (P) , Props ) ) ; c h e ck ( { c o n j , Phi , P s i } , Props ) > case check ( Phi , Props ) of true > check ( Psi , Props ) ; f a l s e > f a l s e ; Phi1 > case check ( Psi , Props ) of true > Phi1 ; f a l s e > f a l s e ; P s i 1 > c o n j ( Phi1 , P s i 1 ) end end ; c h e ck ( { d i s j , Phi , P s i } , Props ) > case check ( Phi , Props ) of true > true ; f a l s e > check ( Psi , Props ) ; Phi1 > case check ( Psi , Props ) of true > true ; f a l s e > Phi1 ; P s i 1 > d i s j ( Phi1 , P s i 1 ) end end ; c h e ck ( { x , Phi } , P r o p s ) > x ( Phi ) ; c h e ck ( { f , Phi } , Props ) > check ( d i s j ( Phi , x ( f ( Phi ) ) ) , Props ) ; c h e ck ( { g , Phi } , Props ) > check ( c o n j ( Phi , x ( g ( Phi ) ) ) , Props ) ; c h e ck ( Phi , P r o p s ) > b a s e : putStrLn ( Unexpected f o r m u l a i n check : ++showLTL ( Phi ) ) .

Auf Grund der hier verwendeten dreiwertigen Logik, k onnen wir Konjunktion und Disjunktion nicht auf die entsprecheden booleschen Operationen zur uck f uhren, sondern m ussen sie selbst implementieren. Die Formel X k onnen wir nicht entscheiden und geben sie unver andert zur uck. Bei den Formeln F und G wickeln wir den Fixpunkt, u ber den diese Formeln deniert sind ab. Dies ist auf Grund der folgenden Aquivalenzen m oglich: |= F gdw. |= X F |= G gdw. |= X G Beachte, dass die sich ergebenden Formeln nur true, f alse, X , Disjunktionen oder Konjunktionen sein k onnen. F , G und prop sind nur unterhalb eines X m oglich.

49

F ur den Fall, dass das System einen Schritt macht, k onnen wir nun in den Formeln das X realisieren: Listing 43: LTL-Implementierung in Erlang
1 2 3 4

s t e p ( { x , Phi } ) > Phi ; s t e p ( { c o n j , Phi , P s i } ) > c o n j ( s t e p ( Phi ) , s t e p ( P s i ) ) ; s t e p ( { d i s j , Phi , P s i } ) > d i s j ( s t e p ( Phi ) , s t e p ( P s i ) ) ; s t e p ( Phi ) > b a s e : putStrLn ( Unexpected f o r m u l a i n s t e p : ++showLTL ( Phi ) ) .

Die letzte Regel sollte eigentlich nicht auftreten und dient nur dem Erkennen von Programm-Fehlern. Nun k onnen wir der Server, welcher f ur das Verwalten und Uberpr ufen der LTL-Formeln zust andig ist, implementieren. Er wird global unter dem Atom ltl registriert, damit seine Pid nicht in jeden Prozess propagiert werden muss und Anwendungen somit einfacher erweitert werden k onnen. Als Zustand speichern wir in diesem Prozess eine Liste von zur Zeit g ultigen Propositionen, sowie die Formeln, welche in den weitern Zust anden noch u uft werden m ussen. Auerdem speichern wir berpr die urspr unglichen mit assert hinzugef ugten Formeln, um diese im Fall einer gefunden Verletzung ausgeben zu k onnen. Alternaitv k onnte man hier sicherlich auch einen String, der die Eigenschaft geeignet bennen hinterlegen. Bei den Propositionen erm oglichen wir es den Prozessen beliebige Propositionen zu setzen bzw. zu l oschen. Zur Vereinfachung vermeiden wir keine doppelten Vorkommen, da wir die Propositionsmenge als einfache Liste implementieren. Zur einfacheren Kommunikation mit dem LTL-Server, stellen wir geeignete Schnittstellen-Funktionen zur Verf ugung. Erkennen wir die Verletzung einer Assertion, geben wir eine Warnmeldung aus. Diese sollte besser in eine spezielle Datei geschrieben werden, da sie sonst in anderen Ein-/Ausgaben des eigentlichen Programms untergeht. Listing 44: LTL-Implementierung in Erlang
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36

s t a r t ( ) > LTL = spawn ( fun ( ) > l t l ( [ ] , [ ] , [ ] ) end ) , r e g i s t e r ( l t l , LTL ) . l t l ( Phis , A s s e r t s , Props ) > receive { a s s e r t , Phi } > case check ( n o r m a l i z e ( Phi ) , Props ) of true > l t l ( Phis , A s s e r t s , Props ) ; f a l s e > ptong ( Phi ) , l t l ( Phis , A s s e r t s , Props ) ; Phi1 > l t l ( [ Phi1 | P h i s ] , [ Phi | A s s e r t s ] , Props ) end ; { newProp , P} > NewProps= [ P | Props ] , P h i s 1 = l i s t s : map( fun ( Phi ) > check ( s t e p ( Phi ) , NewProps ) end , P h i s ) , { Phis2 , A s s e r t s 2 } = a n a l y z e ( Phis1 , A s s e r t s ) , l t l ( Phis2 , A s s e r t s 2 , NewProps ) ; { r e l e a s e P r o p , P} > NewProps= l i s t s : d e l e t e (P , Props ) , P h i s 1 = l i s t s : map( fun ( Phi ) > check ( s t e p ( Phi ) , NewProps ) end , P h i s ) , { Phis2 , A s s e r t s 2 } = a n a l y z e ( Phis1 , A s s e r t s ) , l t l ( Phis2 , A s s e r t s 2 , NewProps ) ; s t a t u s > b a s e : putStrLn ( Unevaluated A s s e r t i o n s : ) , l i s t s : z i p w i t h ( fun ( Phi , A s s e r t ) > b a s e : putStrLn ( showLTL ( A s s e r t ) ) , b a s e : putStrLn ( ++showLTL ( Phi ) ) end , Phis , A s s e r t s ) , l t l ( Phis , A s s e r t s , Props ) end . a s s e r t ( Phi ) > l t l ! { a s s e r t , Phi } . newProp (P) > l t l ! { newProp , P} . r e l e a s e P r o p (P) > l t l ! { r e l e a s e P r o p , P} . s t a t u s ( ) > l t l ! s t a t u s . a n a l y z e ( [ ] , [ ] ) > { [ ] , [ ] } ;

50

37 38 39 40 41 42 43 44 45 46 47

a n a l y z e ( [ true | P h i s ] , [ A s s e r t | A s s e r t s ] ) > a n a l y z e ( Phis , A s s e r t s ) ; a n a l y z e ( [ f a l s e | P h i s ] , [ A s s e r t | A s s e r t s ] ) > potong ( A s s e r t ) , a n a l y z e ( Phis , A s s e r t s ) ; a n a l y z e ( [ Phi | P h i s ] , [ A s s e r t | A s s e r t s ] ) > { Phis1 , A s s e r t s 1 } = a n a l y z e ( Phis , A s s e r t s ) , { [ Phi | P h i s 1 ] , [ A s s e r t | A s s e r t s 1 ] } . ptong ( Phi ) > b a s e : putStrLn ( A s s e r t i o n v i o l a t e d : ++showLTL ( Phi ) ) , e x i t ( 1).

Die Funktion analyze l oscht erf ullt und wiederlegte Formeln aus unseren Listenstrukturen. Im Fehlerfall gibt sie auerdem eine entsprechende Fehlermeldung mittels ptong/1 aus. Abschlieden geben wir dem Benutzer mit der Funktion status noch die M oglichkeit alle noch nicht entschiedenen Formeln auszugeben. Als Beispiel f ur die Benutzung der LTL-Bibliothek betrachten wir den wechselseitigen Ausschluss zweier Prozesse. Auf Grund des Kommunikationsmodells von Erlang ist es nicht so einfach, u berhaupt einen kritischen Bereich zu denieren, in dem sich zwei Prozesse benden. Wir verwenden hierzu einen Speicher(-Prozess), auf dem zwei Prozesse nebenl aug Ver anderungen vornehmen: Listing 45: LTL-Implementierung in Erlang
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27

module ( c r i t i c a l ) . export ( [ s t a r t /0 ] ) . s t a r t ( ) > t r y l t l : s t a r t ( ) catch > 42 end , \% F a l l s LTLS e r v e r schon l a e u f t l t l : a s s e r t ( l t l : g ( l t l : neg ( l t l : c o n j ( l t l : prop ( c s 1 ) , l t l : prop ( c s 2 ) ) ) ) ) , S = spawn ( fun ( ) > s t o r e ( 4 2 ) end ) , spawn ( fun ( ) > i n c ( S ) end ) , dec ( S ) . s t o r e (V) > receive { lookup , P} > P ! V, s t o r e (V ) ; { s e t , V1} > s t o r e (V1) end . i n c ( S ) > S ! { lookup , s e l f ( ) } , receive V > l t l : newProp ( c s 1 ) , S ! { s e t ,V+1} end , l t l : releaseProp ( cs1 ) , inc (S ) . dec ( S ) > S ! { lookup , s e l f ( ) } , receive V > l t l : newProp ( c s 2 ) , S ! { s e t , V1} end , l t l : releaseProp ( cs2 ) , dec ( S ) .

In Abh angigkeit des Schedulings wird die angegebene Eigenschaft nach ein paar Schritten widerlegt. Bei der bisherigen Implementierung haben wir LTL zum Testen von Systemen eingesetzt. Hierbei ergibt sich allerdings das Problem, dass einige Formeln nie als falsch (z.B: F ) bzw. andere nie als korrekt (z.B. G) beweisen werden k onnen. Besonders problematisch sind somit Eigenschaften, welche F oder G verwenden. Sie k onnen durch Testen weder widerlegt noch geeigt werden. M oglichkeiten hiermit umzugehen, ist z.B. obere Schranken einzubauen, nach wie vielen Schritten (Abwicklungen) bzw. f ur wie viele Schritte ein F oder G gelten soll. Auerdem kann es sinnvoll sein, zu protokollieren, wie oft F bzw. G bereits abgewicklet wurden, um einen Eindruck zu bekommen, wie h aug die Bedingung einer Formel schon getestet wurde. Entsprechende Implementierung werden in den Ubungen bsprochen.

51

9.2 Verikation
Urspr unglich wurde LTL aber zur formalen Verikation entwickelt. Betrachtet man endliche Kripke Strukturen ist es n amlich sehr wohl m oglich, Eigenschaften zu beweisen bzw. zu wiederlegen: LTL ist f ur endliche Kripke Strukturen entscheidbar. Das Verfahren, mit dem dieses entschieden wird, nennt man Model Checking, n aheres hierzu ndet man entsprechenden Theorievorlesungen. Beim Model Checking geht man in der Regel davon aus, dasss man ein System (insb. sein Protokoll) mit einer (wenig ausdrucksstarken) Spezikationsprache beschreibt. Deren Semantik deniert eine endliche Kripke Struktur, welche dann zur Uberpr ufung von Eigenschaften (LTL) verwendet werden kann. Ein entsprechendes Tool zur Spezikation/Verikation mittels Model Checking ist z.B. Spin f ur die Sprache Promela und LTL. Ahnliche Tools existieren f ur Prozessalgebren, wie den Calculus of Communicating Systems (CCS) und modale Logiken (CTL, CTL oder den modalen -Kalk ul). Hierbei ist die Idee, dass man zun achst die Spezikation erstellt und gegen spezizierte Eigenschaften veriziert. Danach wird diese dann schrittweise zum tats achlichen System verfeinert. Als Konsequenz wird die Kommunikation dann aber in der Regel nur wenig von konkreten Werten abh angen d urfen, da dies auf Ebende der Spezikation nicht ausgedr uckt werden kann. Spezikationssprachen bieten in der Regel nur u ankten Wertebereich, Datenstrukturen stehen gar nicht zur ber einen sehr eingeschr Verf ugung, da sonst die Semantik keine endliche/kleine Kripke Struktur bilden w urde. Ein andere m oglicher Ansatz zur formalen Verikation ist der umgekehrte Weg. Ausgehend von einem existierenden System, versucht man durch Abstraktion eine endliche Kripke Struktur zu generieren. Die Idee ist hierbei, dass s amtliche Pfade des echten Systems auch in dem abstrahierten endlichen System enthalten sind. Hierzu kann man das Programm anstatt u ber konkreten Weten u ber abstrakten Werten ausf uhren. Bei Verzweigungen, welche dann u ber den abstrakten Werten nicht mehr entschieden werden k onnen, muss man dann um tats achlich alle m oglichen Pfade zu repr asentieren zus atzlichen Nichtdeterminismus verwenden. Da LTL ja auf allen Pfaden eines Systems gelten muss und die Abstraktion eine endliche Kripke Struktur liefern kann, welche alle Pfade der tats achlichen Semantik enth alt, k onnen wir LTL-Formeln mittels Model Checking entscheiden. Hierbei m ussen wir aber beachten, dass f ur den Fall, dass eine LTL-Eigenschaft nicht gilt, zwei m ogliche F alle ber ucksichtigt werden m ussen: Der Pfad, welcher ein Gegenbeispiel darstellt, existiert auch in der konkreten Semantik und zeigt tats achlich einen Fehler an. Der Pfad existiert in der tats achlichen Semantik nicht, da eine Programm-Verzweigung auf Grund der Abstraktion nicht korrekt entschieden wurde. In diesem Fall, kann man durch ein Verfeinerung der Abstraktion versuchen, diese Verzweigung korrekt zu entscheiden, was aber in der Regel zu einer Vergr oerung des Zustandsraums der abstrakten, endlichen Kripke-Struktur f uhren wird. In der Regel wird dieser Ansatz auch genau in den F allen erfolgreich sein, in denen auch eine formale Spezikation m oglich gewesen w are, bei der genau die entsprechenden abstrakten Werte verwendet worden w aren, d.h. in F allen, in denen das Systemverhalten, insbesondere die Kommunikation, wenig von konkreten Werten abh angen.

9.3 Simulation einer Turing-Maschine


Die formale Verikation von Programmen ist schwierig, da fast alle interessanten Programmeigenschaften unentscheidbar sind. Dies gilt bereits f ur sequentielle Programme, welche mit mehr als zwei Zahlenwerten rechnen k onnen oder dynamische Datenstrukturen verwenden k onnen. In diesem Kapitel wollen wir untersuchen, in wie weit auch die Erweiterung einer Programmiersprache um nebenl auge Konzepte die Entscheidung interessanter eigenschaften, wie z.B. der Deadlockfreiheit, unentscheidbar macht. Wir simulieren hierzu allein unter Verwendung einer endlichen Anzahl von Atomen und nur drei Prozessen eine Turingmaschine. Die Idee der Implementierung beruht auf der Darstellung des Bandes der Turing Maschine mit Hilfe zweier Stacks. Beim bewegen des Kontrollkopfes, wird der oberste Wert des einen Stacks auf den anderen Stack verschoben. Die Stacks werden jeweils durch einen Thread implementiert. Ein Thread, welcher die Datenstruktur eines Stacks implementiert, kann wie folgt konstruiert werden: Listing 46: Stack

52

1 2 3 4 5 6

s t a c k (P) > r e c e i v e pop > pop ; X > s t a c k (P) , P ! X, s t a c k (P) end .

Falls der Stack einen pop Nachricht erh alt, tterminiert er, d.h. der darunter liegende Wert wird als Nachricht an einen Kontrollprozess verschickt. Alle anderen Nachrichten werden als push interpretiert und mit Hilfe des Laufzeitkellers gespeichert. Die Funktione push und pop k onnen dann einfach wie folgt deniert werden: Listing 47: Stackoperationen
1 2 3 4 5 6

push ( St ,V) > St !V. pop ( St ) > St ! pop , receive X > X end .

Bei der Verwendung eines Stacks zur Simulation einer Turing Maschine ist aber noch wichtig, dass auch u alt hier ja beliebig viele ber einen leeren Keller hinaus gelesen werden kann. Das Band enth Blanks. Diese Blanks k onnen aber einfach beim pop auf einem leeren Stack geliefert werden: Listing 48: Blank Stack
1 2 3

b l a n k S t a c k (P) > s t a c k (P) , P ! blank , b l a n k S t a c k (P ) .

Nun kann eine Turing Maschine einfach wie folgt deniert werden: Sei M = Q, , , q0 , F eine deterministische Turing Maschine mit der Ubergangsfunktin : Q \ F Q {l, r}. Dann ist kann der Kontrollprozess wie folgt als Erlangfunktion ausgedr uckt werden: F ur all q F , verwende Regel der Form: delta(SL,SR,q ,_) -> pop. F ur alle q Q\F mit (q, a) = (p, b, r): delta(SL,SR,q ,a) -> push(SL,b), A = pop(SR), delta(SL,SR,p,A). und f ur alle q Q \ F mit (q, a) = (p, b, l): delta(SL,SR,q ,a) -> push(SR,b), A = pop(SL), delta(SL,SR,p,A). Die Turing Maschine kann dann wie folgt gestartet werden: start() -> SL=spawn(blankStack,[self()]), SR=spawn(blankStack,[self()]), writeInputToStack(SR), A = pop(SR), delta(SL,SR,q0 ,A), outputStack(SR). Man beachte, dass diese Turing-Maschinen-Simulation nur sehr wenige Werte verwendet. Auer dem Bandalphabet und der Zustandsmenge Q, sind dies das Atom pop und die Pid des Hauptprozesses. Auerdem werden drei Threads verwendet. Es ist sogar m oglich, auf noch einen weiteren Thread zu verzichten. Dies ist jedoch technisch aufw andiger (Ubung).

53

Entsprechend kann man auch mit endrekursiven Erlangprogrammen, die nur endlich viele Werte ver wenden, aber beliebig viele Prozesse erlauben, eine Turingmaschine simulieren (Ubung). Eine weitere M oglichkeit ist die Verwendung eines einzelnen Prozesses und beliebig vielen Werten in der Mailbox, bzw. dynamischen Datenstrukturen. Durch die Simulation der Turingmaschine k onnen zeigen, dass das Model Checking Problem f ur die jeweils betrachtenen Systeme im allgemeinen unentscheidbar ist.

10 Transaktionsbasierte Kommunikation
In diesem Kapitel werden wir uns noch einmal mit den Nachteilen der lockkbasierten Kommunikation besch aftigen und einen alternativen lockfreien Ansatz kennen lernen. Da der Ansatz besonders vielversprechend in Haskell implementiert wurde, betrachten wir in diesem Kapitel zun achst insbesondere Haskell.

10.1 Ein Bankbeispiel in Concurrent Haskell


Aufgabe: Modellierung von Konten und zugeh origen Operationen auf den Konten. Idee: Repr asentation eines Kontos als MVar Int mit folgenden Operationen:
1 2 3 4

withdraw : : MVar I n t > I n t > IO ( ) withdraw a c c amount = do b a l a n c e < takeMVar a c c putMVar a c c ( b a l a n c e amount )

takeMVar lockt quasi die MVar und putMvar gibt sie wieder frei.
1 2 3

d e p o s i t : : MVar I n t > I n t > IO ( ) d e p o s i t a c c amount = withdraw a c c ( amount ) b a l a n c e : : MVar I n t > IO I n t b a l a n c e a c c = readMVar a c c limitedWithdraw : : MVar I n t > I n t > IO Bool limitedWithdraw a c c amount = do b < b a l a n c e a c c i f b >= amount then do withdraw a c c amount r e t u r n True else return False

1 2

1 2 3 4 5 6 7 8

Problem: Die atomare Ausf uhrung ist nicht garantiert. Nach der balance-Abfrage kann ein anderer Thread Geld abheben Kontostand ist zwar immer richtig, kann aber negativ werden. L osung: Lock m usste u ber gesamte limitedWithdraw Aktion erhalten werden keine Wiederverwendung von withdraw m oglich. In Java werden alle Account-Methoden als synchronized gekennzeichnet, so dass eine Wiederverwendung m oglich ist (mehrfaches Nehmen des gleichen Locks ist m oglich). Weitere Operation: sicherer Transfer
1 2 3 4 5 6 7 8 9

l i m i t e d T r a n s f e r : : MVar I n t > Mvar I n t > I n t > IO Bool l i m i t e d T r a n s f e r from t o amount = do b < takeMVar from i f b >= amount then do b < takeMVar t o putMVar from ( b amount ) putMVar t o ( b + amount ) r e t u r n True

54

10 11 12

e l s e do putMVar from b return False

Problem: Falls gleichzeitig von A nach B und von B nach A u berwiesen wird, landen wir im Deadlock. Hier hilft auch in Java das synchronized nicht, da unterschiedliche Locks/Objekte beteiligt sind. Listing 49: L osung
1 2 3 4 5 6 7 8 9 10 11

l i m i t e d T r a n s f e r from t o amount = do b < takeMVar from i f b >= amount then do putMVar from ( b amount ) b < takeMVar t o putMVar t o ( b + amount ) r e t u r n True e l s e do putMVar from b return False

Wobei dies zu einem inkonsistenten Zwischenzustand f uhrt. Eine andere L osung w are alle Locks zu Beginn der Aktion gem a einer globalen Ordnung zu nehmen (zuerst kleine und dann groe Locks nehmen, also immer erst A locken und dann B).
1 2 3 4 5 6 7

i f from < then do b1 < b2 < e l s e do b2 < b1 <

to takeMVar from takeMvar t o takeMVar t o takeMVar from

W ahrend der gesamten Aktion sollten dann keine weiteren Locks erlaubt sein keine Kompositionalit at. 10.1.1 Gefahren bei der Programmierung mit Locks Verwendung von zu wenig Locks Inkonsistenzen Verwendung von zu vielen Locks u aige Sequentialisierung (im besten Fall) und Deadberm locks (im schlechtesten Fall) Verwendung falscher Locks, da oft der Zusammenhang zwischen Lock und zu sichernden Daten fehlt (bei Verwendung zus atzlicher Locks/Lockobjekte in Java) Konsequenzen wie zuvor Nehmen von Locks in falscher Reihenfolge (Race Condition) Deadlock Robustheit, da beim Absturz von Teilkomponenten Locks eventuell nicht mehr freigegeben werden und inkonsistente Systemzust ande zur uck bleiben (z. B. Geld wird vernichtet) Vergessen der Lockfreigabe Deadlock

10.2 Transaktionsbasierte Kommunikation (in Concurrent Haskell als anderen Ansatz)


Transaktionen sind ein bekanntes Konzept bei Datenbanken zur atomaren Ausf uhrung komplexer Datenbankanfragen und -modikationen. Diese Idee kann auch zur nebenl augen Programmierung verwendet werden. Die modizierbaren Variablen entsprechen der Datenbank und die Operationen entsprechen der Transaktion. Transaktionen beeinussen die Welt nur, wenn sie erfolgreich waren. Sie werden in einer eigenen Monade ST M (Software Transactional Memory) deniert. Das atomare Ausf uhren einer Transaktion in der Welt geschieht mittels einer Funktion

55

atomically :: STM a -> IO a. Im Gegensatz zu Datenbanken wird eine ST M immer wieder ausgef uhrt, bis sie erfolgreich war. Zur Kommunikation (anstelle der MVars) dienen spezielle Transaktionsvariablen:
1 2 3 4 5

data TVar a a b s t r a c t newTVar :: a > STM ( TVar a ) readTVar : : TVar a > STM a writeTVar : : TVar a > a > STM ( )

Beachte: Im Gegensatz zu MVars sind TVars niemals leer. Es gibt keinen Lock! Listing 50: Bankbeispiel
1 2 3 4 5 6 7 8 9

ty pe Account = TVar I n t b a l a n c e : : Account > STM I n t b a l a n c e a c c = readTVar a c c withdraw : : Account > I n t > STM ( ) withdraw a c c amount = do b < b a l a n c e a c c writeTVar a c c ( b amount ) d e p o s i t : : Account > I n t > STM( ) d e p o s i t a c c amount = withdraw a c c ( amount ) limitedWithdraw : : Account > I n t > STM Bool limitedWithdraw a c c amount = do b < b a l a n c e a c c i f b >= amount then do withdraw a c c amount r e t u r n True else return False

1 2 3

1 2 3 4 5 6 7 8

Fassen wir nun noch einmal das STM-Interface von Haskell zusammen: Atomare Ausf uhrung einer Transaktion: atomically :: STM a -> IO a TVars als Kommunikationsabstraktion, welche im Rahmen von Transaktionen ver andert werden k onnen: data TVar a ist abstrakt Erstellen, Lesen und Ver andern von TVar-Inhalten: newTVar :: a -> STM (TVar a) readTVar :: TVar a -> STM a writeTVar :: TVar a -> a -> STM ()

10.2.1 Beispielprogramm Listing 51: Beispielprogramm


1 2 3 4 5 6 7 8

main : : IO ( ) main = do a c c 1 < a t o m i c a l l y ( newTVar 1 0 0 ) a c c 2 < a t o m i c a l l y ( newTVar 1 0 0 ) f o r k I O ( do b < a t o m i c a l l y ( limitedWithdraw a c c 1 6 0 ) i f b then r e t u r n ( ) e l s e putStrLn Du b i s t p l e i t e !

56

9 10 11 12

) a t o m i c a l l y ( t r a n s f e r acc1 acc2 50) b < a t o m i c a l l y ( b a l a n c e a c c 1 ) print b

10.3 Synchronisation mit Transaktionen


Als N achstes wollen wir untersuchen, inwieweit Transaktionen auch geeignet sind, Synchronisationsprobleme zu l osen. Hierzu betrachten wir wieder die dinierenden Philosophen und versuchen eine STM-basierte Implementierung zu entwickeln. Zun achst m ussen wir uns u abchen repr asentiert werden k onnen. Da TVars (im berlegen, wie die St Gegensatz zu MVars) nicht leer sein k onnen, ben otigen wir eine TVar, welche boolesche Werte aufnehmen kann. Hierbei bedeutet der Wert True, dasss der Stab verf ugbar ist und False, dass er bereits vergeben ist. Listing 52: Dinierende Philosophen
1

ty pe S t i c k = TVar Bool

// True = l i e g t a u f dem T i s c h ( i n i t i a l )

Als N achstes k onnen wir versuchen Funktionen zum Aufnehmen bzw. Zur ucklegen des St abchens zu denieren. W ahrend putStick sehr einfach deniert werden kann, erreichen wir bei der Denition von takeStick einen Punkt, an dem wir nicht weiter wissen: Listing 53: Dinierende Philosophen
1 2 3 4 5 6 7 8 9 10

p u t S t i c k : : S t i c k > STM ( ) p u t S t i c k s = do writeTVar s True t a k e S t i c k : : S t i c k > STM ( ) t a k e S t i c k s = do b < readTVar s i f b then do writeTVar s F a l s e e l s e ??? // T r a n s a k t i o n e r n e u t v e r s u c h e n

Falls takeStick ausgef uhrt wird, wenn die TVar den Wert False enth alt, kann die gesamte Transaktion nicht mehr erfolgreich werden. An diesem Punkt h atte der lockbasierte Ansatz gewartet (suspendiert), bis ein anderer Prozess den Stab zur uck legt. Denkt man in Transaktionen erkennen wir als Anwender der STM-Bibliothek an dieser Stelle, dass die Transaktion so insgesamt nicht erfolgreich ist, was wir durch Aufruf der Funktion retry :: STM () realisieren k onnen. Bei Aufruf von retry wird die Transaktion erfolglos abgebrochen und erneut gestartet. Inwieweit hierbei tats achlich Busy-Waiting notwendig ist, werden wir sp ater kl aren. Zun achst besagt die Ausf uhrung von retry, dass die Transaktion an dieser Stelle fehlgeschlagen ist: Listing 54: Dinierende Philosophen
1 2 3 4 5 6

t a k e S t i c k : : S t i c k > STM( ) t a k e S t i c k s = do b < readTVar s i f b then do writeTVar s F a l s e else retry // T r a n s a k t i o n e r n e u t v e r s u c h e n

Nun m ussen wir nur noch die Philosophen hinzuf ugen: Listing 55: Dinierende Philosophen
1 2 3 4

p h i l : : S t i c k > S t i c k > IO ( ) p h i l l r = do // denken atomically ( takeStick l )

57

5 6 7 8 9 10

atomically // E a t i n g atomically putStick putStick phil l r

( takeStick r ) ( do l r)

Die einzelnen Transaktionen werden mittels atomically ausgef uhrt. Hierbei f ugen wir beide putStick Aktionen zusammen aus. Unter Hinzunahme einer geeigneten Startfunktion f ur Sticks und Philosophen, l auft das Programm schon sehr schnell nach seinem Start in einen Deadlock. Diesesmal k onnen wir den Deadlock aber sehr viel einfacher vermeiden, als zuvor. Wir m ussen einfach nur fordern, dass beide St abchen atomar, also innerhalb einer Transaktion, genommen werden. Hierzu komponieren wir beide sequentiell auf STM-Ebene und verwenden nur noch einen atomically Aufruf: // Think atomically(do takeStick l takeStick r) Nun l auft das Programm nicht mehr in die Deadlock-Situation. Ein Philosoph versucht, atomar beide St abchen zu nehmen. Ist dies nicht m oglich, wird die gesamte Transaktion neu gestartet. Der Zustand, in dem also das linke St abchen aufgenommen wurde, aber das zweit nicht verf ugbar ist, wird auerhalb der Transition nicht mehr sichtbar.

10.4 MVars
Bei der praktischen Programmierung, ist es aber oft doch recht praktisch, wenn unterschiedliche Prozesse aufeinander warten k onnen. Um dies auch in Transaktionen zu erm oglichen, k onnen wir als n achstes eine MVar auch auf der Ebene der STM zur Verf ugung stellen: Listing 56: MVar in STM
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31

module MVar where import C o n t r o l . Concurrent .STM import C o n t r o l . Concurrent ( f o r k I O ) data MVar a = MVar ( TVar ( Maybe a ) ) d e r i v i n g Eq H i e r d u r c h koennen auch d i e s e MVars v e r g l i c h e n werden (==,/=) newEmptyMVar : : STM (MVar a ) newEmptyMVar = do t < newTVar Nothing r e t u r n (MVar t ) newMVar : : a > STM (MVar a ) newMVar v = do t < newTVar ( J u s t v ) r e t u r n (MVar t ) takeMVar : : MVar a > STM a takeMVar (MVar t ) = do mv < readTVar t case mv o f Nothing > r e t r y J u s t x > do writeTVar t Nothing return x putMVar : : MVar a > a > STM ( ) putMVar (MVar t ) v = do mv < readTVar t case mv o f Just > r e t r y Nothing > writeTVar t ( J u s t v ) readMVar : : MVar a > STM a readMVar m = do v < takeMVar m

58

32 33

putMVar m v return v

Wir verwenden eine ahnliche Idee, wie bei der Realisierung der St abchen. Hierbei m ussen wir allerdings von Bool nach Maybe f ur die gef ullte MVar verallgemeinern. Alle von der MVar bekannten Funktionen sind nun als STM Anweisungen verf ugbar (nicht mehr IO). Die IO-Varianten k onnen einfach mittels atomically deniert werden. Dennoch ist die Denition als STM Anweisungen sinnvoll, da sie so auch elegant mit beliebigen anderen STM Aktionen komponiert werden k onnen. Eine leere MVar modellieren wir also durch eine TVar, welche den Wert Nothing enth alt. Sowohl bei takeMVar als auch bei putMVar kann die Transaktion abgebrochen werden.

10.5 STM-Chan
Als n achster Schritt, ist es nun nat urlich auch m oglich, eine STM-Variante des Channels zu implementieren. Listing 57: Channel mit STM
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35

module Main where import C o n t r o l . Concurrent ( forkIO , t h r e a d D e l a y ) import C o n t r o l . Concurrent .STM import MVar data Chan a = Chan (MVar ( Stream a ) ) r e a d end (MVar ( Stream a ) ) w r i t e end ty pe Stream a = MVar ( ChanElem a ) data ChanElem a = ChanElem a ( Stream a ) newChan : : STM ( Chan a ) newChan = do h o l e < newEmptyMVar r e a d < newMVar h o l e w r i t e < newMVar h o l e r e t u r n ( Chan r e a d w r i t e ) writeChan : : Chan a > a > STM ( ) writeChan ( Chan w r i t e ) v = do newHole <newEmptyMVar o l d H o l e < takeMVar w r i t e putMVar o l d H o l e ( ChanElem v newHole ) putMVar w r i t e newHole readChan : : Chan a > STM a readChan ( Chan r e a d ) = do readEnd < takeMVar r e a d ( ChanElem v newReadEnd ) < readMVar readEnd putMVar r e a d newReadEnd return v isEmptyChan : : Chan a > STM Bool isEmptyChan ( Chan r e a d w r i t e ) = do rEnd < readMVar r e a d wEnd < readMVar w r i t e r e t u r n ( rEnd == wEnd)

Die Implementierung entspricht exakt der schon in Java vorgestellten Implementierung. Allerdings sind auch diese Operationen nun als STM Anweisungen deniert. Werden nun Funktionen, wie readChan oder isEmptyChan verwendet. So ist gew ahrleistet, dass diese nicht mit anderen Transaktionen inferieren. Somit ist der Fehler, den die lockbasierte Chan-Implementierungen bzgl des isEmpty prallel zu einem readChan auf einen leeren Channel hatte, nicht mehr vorhanden.

59

10.6 Alternative Komposition


Haskell bietet eine weitere Abstraktion, welche es erm oglicht, zu fehlgeschlagenen Transaktionen, Alternativtransaktionen zu denieren. orElse :: STM a -> STM a -> STM a Die Idee ist, dass sich orElse t1 t2 (= t1 orElse t2) genau so verh alt, wie t1, falls t1 erfolgreich ist (d.h. kein retry ausf uhrt) und wie t2, falls t1 nicht erfolgreich war und retry ausf uhrte. Als einfache Beispiele f ur die Verwendeung von orElse k onnen wir die MVar-Implementierung um tryTake und tryPut erweitern: Listing 58: tryTakeMVar und tryPutMVar
1 2 3 4 5 6 7 8 9 10 11 12 13

tryTakeMVar : : MVar a > STM ( Maybe a ) tryTakeMVar tVar = ( do v < takeMVar tVar return ( Just v )) orElse r e t u r n Nothing tryPutMVar : : MVar a > a > STM Boolean tryPutMVar tVar v = ( do putMVar tVar v r e t u r n True ) orElse return False

Mit orElse haben wir also, neben der sequentiellen Koposition, eine zweite M oglichkeit, Transaktionen zu komponieren. Beide k onnen geschachtelt verwendet werden, so dass z.B. tryPutMVar auch wieder in Sequenzen oder anderen orElses verwendet werden kann. Als weiteres Beispiel f ur die Verwendung von orElse denieren wir einen simplen zweielementigen Puer: Listing 59: Zweielementiger Puer mit STM
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

module B u f f e r 2 where import C o n t r o l . Concurrent .STM data B u f f e r 2 a = B2 (MVar a ) (MVar a ) n ew B uf f er2 : : STM ( B u f f e r 2 a ) n ew B uf f er2 = do m1 < newEmptyMVar m2 < newEmptyMVar r e t u r n ( B2 m1 m2) r e a d B u f f e r 2 : : B u f f e r 2 a > STM a r e a d B u f f e r 2 ( B2 m1 m2) = takeMVar m1 orElse takeMVar m2 w r i t e B u f f e r 2 : : B u f f e r 2 a > a > STM ( ) w r i t e B u f f e r 2 ( B2 m1 m2) v = putMVar m1 v orElse putMVar m2 v

Der Nachteil dieser Implementierung ist allerdings, dass das FIFO-Prinzip nicht sicher eingehalten wird. Als Ubung kann aber recht einfach eine entsprechende Variante deniert werden.

10.7 Implementierung von STM


Es gibt eine Vielzahl von Implementierungen von Transaktionskonzepten. Hierbei geht man h aug von unterschiedlichen Grundvoraussetzungen aus. Als Beispiel seien hier Datenbank-Transaktionen

60

writeSet atomicallygggggggggg ggreadTVargt1gggggggggggg ggwriteTVargt2g42 ggreadTVargt3 ggwriteTVargt4g43 {?t2,g42)}

readSet {?t1,gv22)} {?t1,gv42),g?t3,gv17)}

{?t2,g42),g?t4,g43)}

GemgglobalergOrdnung

lockg?writeSetg readSet)

validate

rollback unlockg?writeSetg readSet)

gVersionsnummerg allerggelesenengTVarsg nochgaktuell? ja commit unlockg?writeSetg readSet)

nein

gSchreibegwriteSetgingechtegTVars

Abbildung 8: Ablauf einer erfolgreichen Transaktion (dank an Nils Ehmke) genannt, bei welchen bereits vor der Ausf uhrung alle beteiligten Resourcen bekannt sind. Dies k onnen wir f ur unsere Transaktionen nicht fordern, da sie dynamisch w ahrend der Ausf uhrung zusammengesetzt werden. Insbesondere k onnen einzelne gelesene Werte den weiteren Programmablauf beeiussen, was u ollig anderen STM-Aktionen f uhrt, aus denen die ber eine Programmverzweigung bis hin zu v restliche Transaktion besteht. In Haskell ist eine optimistische Ausf uhrungsstrategie f ur die STMs realisiert. W ahrend der Ausf uhrung gehen wir zun achst davon aus, dass wir erfolgreich sein werden und die Transaktion unabh angig von anderen nebenl augen Threads abschlieen k onnen. W ahrend der Ausf uhrung werden also zun achst gar keine Resourcen gelockt. Dies bedeutet aber auch, dass wir zun achst noch keine TVars persistent (also f ur andere Threads sichtbar) ver andern d urfen. Ersatzweise protokollieren wir w ahrend der Ausf uhrung alle writeTVarAktionen mit und k onnen sie dann sp ater am Ende der Transaktion in einer speziellen Commitphase realisieren. Falls wir ein retry erreichen, k onnen wir sie dann entsprechend auch verwerfen. Bevor eine Transaktion in der Commitphase global realisiert werden kann, m ussen wir uns aber noch u onnte es sein, berzeugen, dass sie auf einem konsistenten Zustand des Gesamtsystems basierte. So k dass w ahrend der Ausf uhrung der Transaktion T1 einige TVars gelesen wurden, dann eine andere Transaktion T2 einige TVars (darunter auch die bereits gelesenen andert) und danach T1 noch von T2 ge anderte TVars liest. Dann w are T1 ja auf Basis einer inkonsistenten Sicht durchgelaufen, was bedeutet, dass kein Commit erfolgen darf. In diesem Fall sollte ein Rollback ausgef uhrt, die Anderungen der Transaktion verworfen und die Transaktion erneut gestartet werden. Um feststellen zu k onnen, ob gelesene TVars noch aktuell sind, erweitern wir die TVars um Versionsnummern, welche bei jedem Ver andern einer TVar im Commit hochgez ahlt werden. Zur Konstitenz uberpr ufung k onnen wir dann dem eigentlichen Commit eine Validierungsphase vorschieben, in der f ur alle gelesenen TVars u uft wird, ob die Versionsnummer beim Lesen w ahrend der Ausf uhrung berpr der Transaktion noch aktuell ist. Um dies durchf uhren zu k onnen, protokollieren wir w ahrend der Transaktionsausf uhrung neben den geschriebenen TVars auch die gelesenen TVars und ihre Versionsnummer mit. War die Validierung erfolgreich, f uhren wir das Commit aus, sonst machen wir ein Rollback. Um aber zu verhindern, dass mehrere Commits/Validierungen gleichzeitig ausgef uhrt werden, ist es notwendig diesen kritischen Teil der Transaktion durch Locks zu sichern. M ogliche L osungen sind hier das setzen eines globalen Locks oder das Locken aller gelesenen und geschriebenen TVars. Da zu diesem Zeitpunkt alle gelesenen/geschriebenen TVars bekannt sind, k onen wir Sie gem a einer globalen Ordnung auf den TVars locken und vermeiden Verklemmungen. Bezogen auf die dinnierenden Philosophen entspricht dies der Deadlockvermeidung dadurch, dass ein Philosoph seine St abchen in einer anderen Reihenfolge nimmt, als die anderen.

61

Der gesamte Ansatz ist noch einmal in Abbildung 8 zusammengefasst. In der Vorlesung haben wir den Ansatz in Erlang implementiert. Der Code steht auf der Web-Seite der Vorlesung zur Verf ugung. Eine Besonderheit bei der Implementierung wurde bei der Umsetzung des Chan-Beispiels deutlich. Wie wir ja bereits erl autert haben, sollte der isEmpty-Bug nicht mehr vorhanden sein. Es zeigt sich aber, dass readChan in diesem Beispiel doch blockiert. Auch bei anderen Anwendungen von readChan blockiert der ausf uhrende Thread. Analysiert man stufenweise einmal, welche MVar-Aktionen bei einem readChan ausgef uhrt werden ergibt sich folgende Liste: takeMVar read takeMVar rEnd putMVar read ...

readChan ch

Splittet man dies nun in die hierbei ausgef uhrten TVar-Aktionen auf, ergibt sich folgendes Bild: takeMVar read readChan ch takeMVar rEnd putMVar read ... readTVar writeTVar readTVar writeTVar readTVar writeTVar read read Nothing rEnd rEnd Nothing read read (Just ...)

Die TVar read wird aso zwei Mal gelesen und dazwischen einmal geschrieben. Dies bedeutet, dass die ausgef uhrte Transaktion beim zweiten Lesen eigentlich den bereits lokal ver anderten Zustand verwenden muss. Da dies aber in der ersten Implementierung von readTVar nicht geschieht, sondern erneut der globale, noch unver anderte (gef ullte) MVar-Zustand gelesen wird, suspendiert putMVar auf der noch gef ullten MVar read, anstatt diese wieder mit dem Zeiger auf den neuen Listenanfang zu aktualisieren. Es ist also wichtig, beim lesen von TVars die lokalen Anderungen innerhalb der Transaktion zu ber ucksichtigen, was in der Erlang Implementierung recht einfach realisiert werden kann. Eigentlich erscheint es als u ussig, in Transaktionen einzelne TVars mehr als einmal zu lesen, da ber man sich alternativ ja auch den alten Wert merken k onnte. Auf Grund der Kompositionalit at von STMs, tritt dieser Fall in der Praxis aber doch ofter auf und muss f ur eine korrekte Implementeirung ber ucksichtigt werden.

62

You might also like