Fachbereich Mathematik und Informatik Institut für Informatik Proseminar Nichtsequentielle Programmiersprachen WS 2011/2012 Monitore Sascha Kretzschmann 2. November 2011 Inhaltsverzeichnis 1 Einleitung 2 2 Monitore nach Brinch Hansen / Hoare 3 2.1 Vorüberlegung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 2.2 Monitordefinition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 2.3 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 3 Monitoren in Java 6 3.1 Umsetzung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 3.2 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 4 Anwendungsbeispiel 9 5 Quellen 12 1 1 Einleitung Für nichtsequenzielle Programmierung ist das wichtigste Sicherheitskriterium die Gewährleistung, dass sich Prozesse beim Zugriff auf Variablen nicht gegenseitig interferieren, also beeinflussen. Das bedeutet ferner, dass jeder Zugriff auf gemeinsam genutzte Variablen oder Ressourcen unbedingt von Sperren (locks), Semaphoren o.Ä. überwacht werden muss. Ein Semaphor ist dabei eine Datenstruktur, welches einen fundamentalen Mechanismus zur Synchronisation darstellt. Sie wurden als Verallgemeinerung und Abstraktion von Spin-Locks von Edsger W. Dijkstra vorgeschlagen. Sie können sehr leicht und systematisch dazu benutzt werden, das Problem des gegenseitigen Ausschlusses (mutual exclusion) zu lösen. Jedoch birgt dies einige Probleme und Gefahren. Bei unachtsamer Benutzung kann es nämlich zu Fehlern kommen, die eine kontrollierte Ausführung des Programmes nicht mehr gewährleisten, da es sich lediglich um einen Mechanismus auf niedriger Ebene handelt. Als Beispiele sind dabei, das Vertauschen der P- und V-Operation oder das zu häufige Benutzen derer, anzuführen. Um dieses Problem zu beheben, führte Per Brinch Hansen Monitore als Programmierkonzept ein.[1] Dieses wurde später von C.A.R Hoare populär gemacht.[5] Im ersten Teil meiner Ausarbeitung werde ich besonders auf die Umsetzung von Monitoren nach Brinch Hansen / Hoare eingehen. Der zweite Teil soll darstellen, wie das ursprüngliche Monitor-Konzept in Java umgesetzt wird, bzw. Unterschiede herausarbeiten. Am Ende skizziere ich noch ein einfaches Beispiel. 2 2 Monitore nach Brinch Hansen / Hoare 2.1 Vorüberlegung Per Brinch Hansen wollte die sequentielle Programmiersprache Pascal um nichtsequentielle Elemente erweitern.[1] Sein Hauptanliegen war dabei, ein Sprachkonzept zu entwickeln, indem parallele Prozesse Daten in einem gemeinsamen Speicher teilen können. Mehrere Prozesse sollen also Daten gemeinsam nutzen können. Dazu betrachten wir einen Prozess detaillierter. Ein Prozess besteht aus Zugriffsrechten, einer privaten Datenstruktur und einem sequentiellen Programm, welches abgearbeitet wird. Offensichtlich arbeitet es auf seinen privaten Daten. Jedoch kann ein Prozess nicht auf die private Datenstruktur eines anderen Prozesses zugreifen. Wie können also konkurrierende Prozesse Daten gemeinsam nutzen, zum Beispiel über einen gemeinsamen Puffer? Die Lösung ist zunächst trivial. Es existiert ein Puffer und zwei Prozesse in Form eines producer process und eines consumer process, wobei der Produzent Daten erzeugt und diese im Puffer ablegt und der Konsument diese aufruft.[1] Folglich können konsumierende Prozesse Daten aufrufen und verarbeiten, solange diese existieren. Produzierende Prozesse hingegen stellen Daten zur Verfügung, solange der gemeinsam benutzte Puffer nicht voll ist. Doch wie soll jetzt der Puffer umgesetzt werden, so dass gegenseitiger Ausschluss (mutual exclusion) sichergestellt wird? Die Lösung sind Monitore. 2.2 Monitordefinition Ein Monitor ist ein Konstrukt, welches parallele Prozesse synchronisiert und ggf. die Zugriffsreihenfolge kontrolliert. D.h. es wird mutual exclusion gewährleistet, so dass zu jeder Zeit immer nur genau ein Prozess Code einer Prozedur ausführt.[3] Ein Monitor definiert Variablen, auf die man nur über die deklarierten Monitor-Prozesse zugreifen kann. Zusätzlich gibt es die Möglichkeit, Bedingungsvariablen (condition variables) zu definieren. 3 2 Monitore nach Brinch Hansen / Hoare Auf diese können nun folgende Operationen ausgeführt werden: 1. wait(c): warte, bis ein Prozess signalisiert, dass Bedingung c erfüllt ist 2. signal(c): signalisiere einen Prozess, der gerade auf c wartet 3. signalAll(c): wie signal(c), nur, dass alle Prozesse signalisiert werden 4. empty(c): überprüfe, ob ein Prozess gerade auf Bedingung c wartet Beim Signalisieren von Prozessen kann man weiterhin vier verschiedene Möglichkeiten des Fortführens unterscheiden: 1. Signal-and-continue: Der Signalisierende fährt fort und der aufgeweckte Prozess ist zu einem späteren Zeitpunkt dran, wenn Monitor frei ist. 2. Signal-and-wait: Der signalisierende Prozess wartet und ist zu einem späteren Zeitpunkt dran. Der aufgeweckte Prozess kann nun arbeiten. 3. Signal-and-urgent-wait: wie oben, nur das dem signalisiertem Prozess garantiert wird, dass er nach dem aufgeweckten Prozess dran wird (wird vorne an die Queue gesetzt) 4. Signal-and-return: Die Signalisierung ist nur mit dem Verlassen eines Prozesses aus dem Monitor verbunden. Wollen wir uns nun einmal verschiedene "Typen"von Monitoren anschauen, die teilweise die eben genannten Varianten des Signalisierens umsetzen. Es existieren nämlich verschiedene Möglichkeiten, wie Prozesse in einem Monitor verwaltet werden. Zu den populärsten gehört einmal die Variante nach Hoare: Hoare style monitor[3] 4 2 Monitore nach Brinch Hansen / Hoare Diese Art von Monitoren besitzt eine Eingangswarteschlange und eine Warteschlange für jede Bedingungsvariable. Sobald ein Prozess den Monitor betritt, wird er zunächst in die Eingangswarteschlange eingereiht. Dort verbleibt er, solange ein anderer Prozess Code in der kritischen Sektion ausführt. Wird zum Beispiel wait(s) aufgerufen, landet der Prozess in der Warteschlange für die Bedingungsvariable s. Die zweite Variante stammt von Mesa: Mesa style monitor[3] Mesa benutzt die signal-and-continue Variante mit einer FIFO Warteschlangen. Im Gegensatz zu Hoare, landen also signalisierte Prozesse zunächst einmal wieder in der Eingangswarteschlange. Auch Java Monitore sind eine Variation dieses Types. 2.3 Zusammenfassung Im vorherigen wurden Monitore beschrieben und gezeigt, wie diese zusammen mit Klassen und Prozessen in Concurrent Pascal umgesetzt wurden. Dabei sollten sich einige entscheidende Punkte herausgestellt haben. 1. Monitore sind historisch das erste nicht triviale Sprachkonzept für Synchronisation. 2. Jede condition variable bringt eine eigene Warteschlange, in der Prozesse separat verzögert werden können, um auf bestimmte Bedingungen zu warten. 3. Wird eine Bedingung erfüllt, bekommen wartende Prozesse ein Signal. 4. Dabei gibt es vier verschiedene Varianten des Signalisierens, die Einfluss auf die Ausführungsreihenfolge haben (können). 5 3 Monitoren in Java 3.1 Umsetzung Um in Java auch Nichtsequenzialität benutzen zu können, wurden Threads eingeführt. Ein Thread kann man sich als ein abgekapseltes Objekt vorstellen, welches sequenziellen Code abarbeitet. In Java existiert kein Konstrukt, mit dem man Monitore explizit erzeugen kann. Somit kann man so gut wie jedes Objekt als einen Monitor verwenden. Jedoch wird dann kein gegenseitiger Ausschluss garantiert. Wir wollen uns also zu Beginn erst einmal anschauen, was alles getan werden muss, um "normale"Java Klassen in eine Java Monitor Klasse zu überführen. 1. Instanzvariablen der Klassen müssen private deklariert sein 2. Methoden müssen als synchronized deklariert werden 3. Bedingte Synchronisation mit wait(), notify() und notifyAll() Folglich unterscheidet sich die Umsetzung von Monitoren in Java stark, von der eigentlichen Auffassung nach Brinch Hansen/Hoare.[1] Das macht die ganze Sache relativ unsicher und garantiert keinen leichten und sicheren Umgang mehr, so wie es ursprünglich gedacht war. Weiterhin existieren keine Bedingungsvariablen. Stattdessen hat jedes Objekt einen Lock, der nur gehalten werden kann, wenn die Methoden synchronized deklariert wurden.[4] Neben einer Eingangswarteschlange (entry queue) existiert lediglich eine wait queue, in der alle wartenden Threads verwaltet werden. 6 3 Monitoren in Java Java style monitor[3] Nun kann es aber sein, dass nach der Prüfung einer bestimmten Bedingung, ein Thread verzögert werden muss. Dazu werden die oben genannten drei Methoden zur bedingten Synchronisation verwendet. Diese wären (sei o eine Ausprägung vom Typ Object): 1. o.wait(): lock von o wird freigegeben, Thread gelangt in Queue 2. o.release(): weckt einen Thread auf, der dann wartet, bis er Lock von o bekommt 3. o.releaseAll():wie release(), nur werden alle Threads aufgeweckt Mit anderen Worten, man synchronisiert also über die Locks der Objekte. Und das kann man sich so vorstellen. Beim Aufruf einer synchronized Methode, eignet (acquire) sich der Thread den Lock an und kann unter gegenseitigem Ausschluss die gemeinsam genutzten Daten manipulieren. Danach wird der Lock wieder freigegeben.[4] 3.2 Zusammenfassung Abschließend noch einmal zusammengefasst, inwiefern sich das Monitorkonzept in Java vom "klassischen"Monitor nach Brinch Hansen insbesondere unterscheidet. 1. In Java existiert kein Monitorkonstrukt, sowie keine Bedingungsvariablen. 2. Die Umsetzung eines Monitors in Java ist immer eine Variation des Monitores nach Mesa mit signal-and-continue. 7 3 Monitoren in Java 3. Bedingungsvariablen können simuliert werden, indem über das Lock von Objekten synchronisiert wird. 4. Sind jedoch alle Methoden einer Klasse synchronized deklariert, entspricht dies der klassischen Monitorvariante. 5. Java Monitore sind eine Variante nach Mesa, mit signal-and-continue. 6. Fairness wird in Java nicht "betrachtet", anders gesagt kann darüber keine Aussage getroffen werden. 8 4 Anwendungsbeispiel Hier die Umsetzung eines Bounded Buffers in Concurrent Pascal und in Java: bounded b u f f e r : monitor begin b u f f e r : array O . . N − 1 of p o r t i o n ; lastpointer : O. .N − 1; count : O . . N; nonempty , n o n f u l l : c o n d i t i o n ; procedure append ( x : p o r t i o n ) ; begin i f count = N then n o n f u l l . w a i t ; n o t e 0 <= count < N; b u f f e r [ l a s t p o i n t e r ] := x ; l a s t p o i n t e r := l a s t p o i n t e r o l ; count := count + 1 ; nonempty . s i g n a l end append ; procedure remove ( r e s u l t x : p o r t i o n ) ; begin i f count = 0 then nonempty . w a i t ; n o t e 0 < count < N; x := b u f f e r [ l a s t p o i n t e r o count ] ; nonfull . signal end remove ; count := 0 ; l a s t p o i n t e r := 0 ; end bounded b u f f e r ; Bounded buffer in Java[5] Es gibt Produzenten, die Daten erzeugen und diese an Konsumenten weitergeben.Die Weitergabe erfolgt mit Hilfe des Buffers, der eine bestimmte Anzahl von Daten lagern kann. Es muss darauf geachtet werden, dass der Produzent wartet, sollte der Buffer voll sein. Hierfür existiert die Bedingungsvariable nonfull. Sollte der Puffer also voll sein, dann wartet der Prozess in der Warteschlange der Bedingungsvariable. Er wird erst wieder signalisiert, wenn ein Prozess die Methode remove() aufgerufen hat, und somit wieder etwas prouziert werden kann. 9 4 Anwendungsbeispiel Außerdem sollen Konsumenten, die einen leeren Buffer vorfinden, ebenfalls warten. Für diesen Fall gibt es die Bedingungsvariable nonempty. Das Prinzip ist das selbe, wie bei nonfull. Im folgenden sehen wir nun die Umsetzung des Bounded Buffers in Java: class Buffer { private Object [ ] b u f f e r ; // Das O b j e c t b u f f e r h a t d i e i n d i z e s i n und o u t private int in , out , count , s i z e ; public synchronized void append ( Object { while ( count == s i z e ) w a i t ( ) ; // buffer [ in ] = o ; in = ( in + 1) % s i z e ; // ++count ; // notifyAll (); // } public synchronized Object remove ( ) { while ( count == 0 ) w a i t ( ) ; Object o = b u f f e r [ out ] ; b u f f e r [ out ] = null ; out = ( out + 1 ) % s i z e ; −−count ; notifyAll (); return o ; } o) Buffer i s t v o l l Produziert Erhoehe c o u n t e r Wecke a l l e wartenden Threads // B u f f e r i s t l e e r // Konsumiert // Senke c o u n t e r // Wecke a l l e wartenden Threads public B u f f e r ( ) { t h i s . i n = 0 ; t h i s . out = 0 ; t h i s . count = 0 ; t h i s . s i z e = 1 0 ; } } Bounded buffer in Java[4] Bei dieser Umsetzung existiert nur eine Warteschlange, nämlich die implizize Bedingungsvariable des Objektes (wait queue). Da nun beim Signalisieren nicht auf spezielle Warteschlangen 10 4 Anwendungsbeispiel zugegriffen werden kann (alle befinden sich in einer), werden alle wartenden Prozesse mit der releaseAll()-Methode aufgeweckt und nach dem Prinzip signal-and-continue behandelt. Ein weiterer Unterschied ist, dass die Bedingungen über while-Schleifen überprüft werden. Es kann nämlich passieren, dass ohne Aufruf der releaseAll()-Methode ein wartender Prozesse aufgeweckt wird. Um Probleme durch dieses unvorhersehbare Verhalten zu umgehen, haben wir die ebend erwähnte Überprüfung umgesetzt (covering condition). 11 5 Quellen Brinch Hansen, P., 1975a. The programming language Concurrent Pascal. IEEE Transactions on Software Engineering 1, June, 199-207; http://dl.acm.org/citation.cfm?id=762983 [1] Brinch Hansen, P., Java’s insecure parallelism, SIGPLAN Notices 34, 4 (April 1999), 38-45. Copyright 1999, http://www.margull.com/downloads/PBHansenParallelism.pdf [2] Kyas, Prof. Dr. Marcel 2011, Manuskript zur Vorlesung ALP4, Freie Universität Berlin, Abschnitt über Monitore [3] Gorlatsch, Sergei 2006, Multithreating and Networking im Java-Umfeld Volresung 4, Uni Münster, 11-24; http://pvs.uni-muenster.de/pvs/lehre/WS08/mnj/folien/mnj4.pdf [4] C.A.R. Hoare: Monitors – An Operating System Structuring Concept. Comm. of the ACM 17.10, October 1974; ftp://reports.stanford.edu/pub/cstr/reports/cs/tr/73/401/CS-TR73-401.pdf [5] 12