Christian-Albrechts-Universität zu Kiel Diplomarbeit Erweiterung des Concurrent Haskell Debuggers für transaktionsbasierte Kommunikation Fabian Reck 29. April 2008 Institut für Informatik Lehrstuhl für Programmiersprachen und Übersetzerkonstruktion betreut durch: Priv.-Doz. Dr. Frank Huch ii Eidesstattliche Erklärung Hiermit erkläre ich an Eides statt, dass ich die vorliegende Arbeit selbstständig verfasst und keine anderen als die angegebenen Hilfsmittel verwendet habe. Kiel, iv Inhaltsverzeichnis 1 Einleitung 1 2 Einführung in Haskell 3 2.1 2.2 Funktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.1.1 Funktionen höherer Ordnung 2.1.2 Anonyme Funktionen 4 . . . . . . . . . . . . . . . . . . 4 . . . . . . . . . . . . . . . . . . . . . . 5 2.1.3 Partielle Applikation . . . . . . . . . . . . . . . . . . . . . . . 5 2.1.4 Pattern Matching . . . . . . . . . . . . . . . . . . . . . . . . . 6 Das Typsystem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 2.2.1 Datentypen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 2.2.2 Polymorphie . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 2.2.3 Typklassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 2.3 Verzögerte Auswertung . . . . . . . . . . . . . . . . . . . . . . . . . . 8 2.4 Monaden 9 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.4.1 Die IO-Monade . . . . . . . . . . . . . . . . . . . . . . . . . . 9 2.4.2 Allgemeine Monaden . . . . . . . . . . . . . . . . . . . . . . . 13 2.5 Speicherverwaltung . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14 2.6 Das Modulsystem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14 3 Concurrent Haskell 15 3.1 Threads . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 3.2 Kommunikation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 3.3 Dinierende Philosophen in Concurrent Haskell . . . . . . . . . . . . . 17 4 Transaktionen in Haskell 4.1 4.2 21 Original-Bibliothek . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 4.1.1 Grundlegende Transaktionen . . . . . . . . . . . . . . . . . . 22 4.1.2 Sequenzielle Komposition . . . . . . . . . . . . . . . . . . . . 22 4.1.3 Blockieren von Transaktionen . . . . . . . . . . . . . . . . . . 23 4.1.4 Alternative Komposition . . . . . . . . . . . . . . . . . . . . . 24 4.1.5 Exceptions . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25 4.1.6 Invarianten . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25 Lightweight-Bibliothek für Transaktionen in Haskell . . . . . . . . . 26 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27 4.2.2 Konsistenzprüfung und Ausführung . . . . . . . . . . . . . . . 29 4.2.3 retry 30 4.2.1 TVars . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . v Inhaltsverzeichnis 4.3 4.2.4 orElse . 4.2.5 Exceptions . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33 4.2.6 Bekannte Nachteile . . . . . . . . . . . . . . . . . . . . . . . . 33 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Erweiterung der Transaktionsbibliothek um Invarianten . . . . . . . 34 4.3.1 Erzeugen von Invarianten . . . . . . . . . . . . . . . . . . . . 34 4.3.2 Überprüfung von Invarianten am Ende von Transaktionen . . 35 4.3.3 Probleme bei der Invariantenprüfung . . . . . . . . . . . . . . 37 5 Debugging in Haskell 5.1 5.2 5.3 41 Herkömmliche Debugger . . . . . . . . . . . . . . . . . . . . . . . . . 41 5.1.1 HOOD . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41 5.1.2 Hat 41 5.1.3 Buddha . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Concurrent Haskell Debugger 6.2 6.3 vi 44 . . . . . . . . . . . . . . . . . . . . . . 45 5.2.1 Starten des Debuggers . . . . . . . . . . . . . . . . . . . . . . 45 5.2.2 Das Hauptfenster . . . . . . . . . . . . . . . . . . . . . . . . . 45 5.2.3 Die Quelltextanzeige . . . . . . . . . . . . . . . . . . . . . . . 49 5.2.4 Funktionsweise 49 . . . . . . . . . . . . . . . . . . . . . . . . . . Concurrent Haskell Stepper . . . . . . . . . . . . . . . . . . . . . . . 54 5.3.1 Prinzip der Deadlocksuche . . . . . . . . . . . . . . . . . . . . 54 5.3.2 Redenition der IO-Monade . . . . . . . . . . . . . . . . . . . 55 5.3.3 Suche nach Deadlocks . . . . . . . . . . . . . . . . . . . . . . 58 5.3.4 Reduzierung des Suchraums . . . . . . . . . . . . . . . . . . . 61 5.3.5 Integration des CHS in den CHD . . . . . . . . . . . . . . . . 63 5.3.6 Exceptionhandling im CHS 65 5.3.7 Unterstützung von im CHS . . . . . . . . . 68 5.3.8 Einschränkungen des CHS . . . . . . . . . . . . . . . . . . . . 69 . . . . . . . . . . . . . . . . . . . unsafePerformIO 6 Debuggen von Transaktionen 6.1 31 71 Darstellung von globalen Aktionen TVar . . . . . . . . . . . . . . . . . . . 71 . . . . . . . . . . . . . . . . . . . . . . . . . 72 6.1.1 Lesen einer 6.1.2 Suspendieren durch . . . . . . . . . . . . . . . . . . . . 72 6.1.3 Abschluss einer Transaktion . . . . . . . . . . . . . . . . . . . 73 6.1.4 Neustart einer Transaktion bei inkonsistenter Sicht . . . . . . 73 6.1.5 Propagieren einer Exception . . . . . . . . . . . . . . . . . . . 73 6.1.6 Entfernen von retry TVars . . . . . . . . . . . . . . . . . . . . . . . 73 Darstellung von lokalen Aktionen . . . . . . . . . . . . . . . . . . . . 74 TVar 6.2.1 Erzeugen einer neuer . . . . . . . . . . . . . . . . . . . . 74 6.2.2 Schreibaktionen . . . . . . . . . . . . . . . . . . . . . . . . . . 75 6.2.3 Lesen einer bereits geschriebenen . . . . . . . . . . . . . 75 6.2.4 Alternative Ausführung mit . . . . . . . . . . . . . 75 6.2.5 Erzeugen und Testen von Invarianten . . . . . . . . . . . . . . 75 6.2.6 Abfangen von Exceptions 76 TVar orElse . . . . . . . . . . . . . . . . . . . . . Überspringen der Transaktionsschritte . . . . . . . . . . . . . . . . . 76 Inhaltsverzeichnis 6.4 Transparente STM-Bibliothek . . . . . . . . . . . . . . . . . . . . . . 7 Implementierung des Debuggers für Transaktionen 7.1 Erweiterung des Debuggers 7.1.1 7.1.2 7.2 Erzeugen einer neuen TVar TVar 77 . . . . . . . . . . . . . . . . . 78 . . . . . . . . . . . . . . . . . . . 79 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82 7.1.4 orElse . . catchSTM 7.1.5 Erzeugen von Invarianten 7.1.6 atomically 7.1.7 Unterbinden von Nachrichten an den Debugger 7.1.3 77 . . . . . . . . . . . . . . . . . . . . . . . Schreiben und Lesen einer 76 . . . . . . . . . . . . . . . . . . . . 83 . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83 . . . . . . . . 87 . . . . . . . . . . . . . . . . . . . . . . . . 88 7.2.1 Deadlocksuche mit Transaktionen . . . . . . . . . . . . . . . . 89 7.2.2 Partial Order Reduction . . . . . . . . . . . . . . . . . . . . . 92 7.2.3 Integration in den CHD 95 Erweiterung des Steppers . . . . . . . . . . . . . . . . . . . . . 8 Zusammenfassung und Ausblick 101 Inhalt der CD 103 Listings 105 Abbildungsverzeichnis 109 Literaturverzeichnis 111 vii Inhaltsverzeichnis viii 1 Einleitung Heutzutage spielt Nebenläugkeit in vielen Softwareanwendungen eine Rolle. Programme mit graschen Benutzeroberächen sollen, egal was sie gerade machen, möglichst ohne Verzögerung auf Benutzereingaben reagieren. Wichtig ist Nebenläugkeit auch im Serverbereich, Server sollen häug auf eine groÿen Zahl von Anfragen gleichzeitig reagieren. Eine Anfrage soll den Server nicht blockieren, bis sie komplett abgearbeitet ist. Doch das Entwickeln von nebenläugen Programmen bringt auch eine Reihe von Problemen mit sich. Wird von verschiedenen Teilen eines nebenläugen Programms auf dieselbe Ressource zugegrien, so kann es leicht passieren, dass diese Ressource auf unzulässige Weise benutzt wird. Um dies zu verhindern, werden solche Ressourcen oft dadurch geschützt, dass sich ein Programmteil den exclusiven Zugri auf diese sichern kann. Andere Programmteile, die die Ressource gleichzeitig nutzen wollen, müssen warten, bis diese wieder freigegeben wurde. Dieses Vorgehen bringt jedoch seinerseits wieder potenzielle Fehlerquellen mit sich. So kann es passieren, dass verschiedene Programmteile gegenseitig darauf warten, dass der jeweils andere Programmteil eine Ressource wieder freigibt, dies wird als Verklemmung oder auch Deadlock bezeichnet. Auch wenn darauf geachtet wurde, dass Ressourcen in jedem Fall wieder freigegeben werden, kann es immer noch vorkommen, dass sich verschiedene Programmteile eine Ressource immer wieder wegnehmen. Das Programm wird dann in einem sogenannten Livelock endlos ausgeführt, ohne dass ein echter Fortschritt erreicht wird. Da bei vielen nebenläugen Programmen ein Scheduler bestimmt, in welcher Reihenfolge und wie lange ein Teil eines nebenläugen Programms ausgeführt wird, ist das Verhalten des Programms nicht deterministisch. Für einen Entwickler ist es daher sehr schwierig, die eben erwähnten Fehler zu vermeiden. Er muss sicherstellen, dass sich sein Programm bei jeder erdenklichen Ausführungsreihenfolge seiner Teile korrekt verhält. Da dies auch schon bei kleineren Programmen alles andere als einfach ist, werden häug Werkzeuge, auch Debugger genannt, verwendet, um das Verhalten von nebenläugen Programmen zu verstehen und Fehler zu nden. Durch die Erweiterung Concurrent Haskell [17] bietet auch die rein funktionale Programmiersprache Haskell [10] die Möglichkeit, nebenläuge Programme zu entwickeln. Um auch das Verhalten von in Haskell geschriebenen nebenläugen Programmen durch ein Werkzeug darstellen zu können, wurde der Concurrent Haskell Debugger (CHD)[1, 2] entwickelt. Dieser ermöglicht ein interaktives Ausführen von neben- läugen Haskell Programmen und stellt den jeweiligen Programmzustand grasch 1 1 Einleitung dar. Später wurde der CHD noch um den Concurrent Haskell Stepper (CHS) [4], eine im Hintergrund ablaufende Deadlocksuche, erweitert. Um einige der Fehlerquellen in nebenläugen Programmen gleich ganz zu beseitigen oder zumindest die Entwicklung solcher Programme zu erleichtern, kann das von Datenbanken bekannte Prinzip der Transaktionen auch in der nebenläugen Programmierung verwendet werden. Die Idee beim sogenannten Software Transactional Memory (STM) ist, dass bestimmte Quellcodeabschnitte als atomar auszuführend gekennzeichnet werden. So gekennzeichnete Abschnitte sollen sich verhalten, als ob sie eine einzige unteilbare Aktion darstellen. Dadurch können Deadlocks und durch abgebrochene Programmteile verursachte inkonsistente Programmzustände verhindert werden. Als Composable Memory Transactions [8] sind Transaktionen, erweitert um Kombinierbarkeit und die Möglichkeit, einen nebenläugen Programmteil zu blockieren, im Glasgow Haskell Compiler (ghc) [6] seit Version 6.4 verfügbar. Doch auch wenn das Entwickeln von nebenläugen Programmen durch Transaktionen erleichtert wird, kann es immer noch schwierig sein, deren Verhalten zu verstehen. Da Transaktionen in Haskell auch blockieren können, ist es auch immer noch möglich, dass Deadlocks auftreten. Um auch für nebenläuge Haskell Programme, die Transaktionen beinhalten, ein interaktives Debuggen, wie mit dem Concurrent Haskell Debugger, zu erlauben, wird in dieser Arbeit erläutert, wie dieser um die Darstellung von Transaktionen erweitert werden kann. Ziel ist es, mit dem erweiterten Debugger das Verhalten ganzer Programme, einzelner Transaktionen, aber auch, zum Beispiel für Lehrzwecke, die Funktionsweise der Transaktionsimplementierung für Haskell verstehen zu können. Um immer noch im Hintergrund nach Deadlocks suchen zu können, wurde auch der Concurrent Haskell Stepper erweitert. 2 2 Einführung in Haskell Haskell [10] ist eine rein funktionale, streng getypte Programmiersprache. Sie wurde von einem auf der 1987 in Portland abgehaltenen Konferenz Functional Programming Languages and Computer Architecture (FPCA '87) eingesetzten Komitee entwickelt. Das Komitee sollte die damals entstandene Vielfalt an rein funktionalen Programmiersprachen auf eine gemeinsame Basis stellen, um damit die breite Nutzung dieser Art von Programmiersprachen voranzutreiben. Die vom Komitee entwickelte Programmiersprache sollte sowohl in Lehre und Forschung als auch zur Entwicklung groÿer Applikationen einsetzbar und für jedermann frei verfügbar sein. Das Ergebnis dieser Bemühungen wurde nach dem Logiker Haskell B. Curry benannt und am 1. April 1990 als Haskell version 1.0 report veröentlicht. In den folgenden Jahren entwickelte sich die Sprache weiter, bis 1997 auf dem Haskell Workshop in Amsterdam entschieden wurde, eine stabile Sprachversion namens Haskell 98 zu etablieren. Zusätzlich zur eigentlichen Sprachdenition wurden eine Reihe von Bibliotheken mit in den Standard aufgenommen, um Programmen unter anderem einheitliche Mechanismen zur Ein-/Ausgabe und zur Interaktion mit dem Betriebssystem zu ermöglichen. Dieser Standard wurde im Februar 1999 als Haskell 98 report und Haskell 98 library report veröentlicht. Nach der Verbesserung von Fehlern und einigen anderen Klarstellungen wurden die beiden Dokumente 2003 im Revised Haskell 98 Report [16] zusammengefasst. Auch nach der Veröentlichung des stabilen Sprachstandards geht die Entwicklung von Haskell weiter, so unterstützen viele Implementierungen neben Haskell 98 auch eine Reihe von Erweiterungen, zu denen auch das in dieser Arbeit wichtige Concurrent Haskell gehört. Auÿerdem wird an einem Nachfolger für Haskell 98 namens Haskell' (Haskell Prime) [11] gearbeitet. Weitere interessante Informationen über Haskell und dessen Geschichte enthält A history of Haskell: being lazy with class von Hudak et. al [15]. Einige der Beispiele in diesem Kapitel sind der Vorlesungsmitschrift von Klaas Ole Kürtz von Frank Huchs Vorlesung Funktionale Programmierung [14] entnommen. 3 2 Einführung in Haskell 2.1 Funktionen In einer rein funktionalen Sprache sind Funktionen das wichtigste Ausdrucksmittel. In Haskell erhalten Funktionen eine festgelegte Anzahl von Argumenten und liefern ein deterministisches Ergebnis. Insbesondere haben Funktionen keine Seiteneekte. fib n = if n == 0 then 0 else if n == 1 then 1 else fib ( n - 2) + fib (n - 1) Listing 2.1: Fibonacci Die in Listing 2.1 dargestellte Funktion berechnet die n-te Fibonacci-Zahl. Häug kann in Funktionsdenitionen auf Klammern weitgehend verzichtet werden. Zu beachten ist allerdings, dass im Gegensatz zu den meisten anderen Programmiersprachen die Einrückung von Bedeutung ist. So kann in diesem Beispiel der Compiler feststellen, dass der Ausdruck + fib (n - 1) zum zweiten else-Fall gehört. 2.1.1 Funktionen höherer Ordnung Eine wichtige Eigenschaft von Haskell ist die Unterstützung von Funktionen höherer Ordnung, also Funktionen, die als Argumente wieder Funktionen erhalten. Ein gutes Beispiel für eine solche Funktion ist map, die eine übergebene Funktion auf jedes Element einer Liste anwendet. succList list = map succ list Listing 2.2: Anwendungsbeispiel für Hier wird mithilfe von map map eine Funktion deniert, die eine Liste berechnet, die die Nachfolger der übergebenen Liste enthält. Die Implementierung von map verdeutlicht den Nutzen von Funktionen höherer Ordnung. succList list = if null list then [] else succ ( head list ) : succList ( tail list ) Listing 2.3: 4 succList ohne map succList ohne 2.1 Funktionen 2.1.2 Anonyme Funktionen Funktionen lassen sich auch anonym deklarieren. Die Syntax hierfür wurde von den Lambda-Abstraktionen des Lambda-Kalküls von Church [5] inspiriert. Die LambdaAbstraktion dient zur Beschreibung von Funktionen mit einem Argument. Der Lambda-Ausdruck λx.x + 1 ordnet dem Argument x das Ergebnis x+1 (2.1) zu. Listing 2.4 zeigt die entsprechende anonyme Funktion in Haskell. Das den Ausdruck einleitende Zeichen '\' soll dabei (\ x -> x + 1) Listing 2.4: Lambda-Ausdruck in Haskell das groÿe Lambda darstellen. Anstelle des Punktes steht ein Pfeil, da der Punkt für die Hintereinanderausführung von Funktionen genutzt wird. Auÿerdem kann der Lambda-Ausdruck in Haskell, im Gegensatz zu seinem Vorbild, auch mehr als nur ein Argument haben. 2.1.3 Partielle Applikation Das für Haskell wichtige Prinzip der partiellen Applikation basiert auf einem Verfahren, das von Moses Schönnkel [20] zuerst entwickelt wurde, jedoch wie Haskell selbst nach Haskell B. Curry, der das Verfahren später unabhängig entwickelte, Currying genannt wird. Beim Currying geht es im Prinzip darum, dass eine Funktion mit n>1 Argumenten in eine Funktion umgewandelt wird, die ein Argument erwartet und als Ergebnis eine Funktion mit n−1 Argumenten liefert. f : (X × Y ) → Z, f (x, y) = z (2.2) fc : X → (Y → Z), fc (x)(y) = z (2.3) f mit zwei Argumenten, die angewandt auf (x, y) den Wert z liefert. In 2.3 stellt fc die durch das Currying umgewandelte Version von f dar, sie liefert angewandt auf das Argument x eine Funktion, die angewandt auf y dasselbe Ergebnis z liefert wie die ursprüngliche Funktion. Gleichung 2.2 zeigt hier eine Funktion Dieses Verfahren ermöglicht nun, dass sich die Funktion succList aus 2.2 nun auch wie in 2.5 denieren lässt. succList = map succ Listing 2.5: Hierbei wird map succList mit partieller Applikation partiell auf die Funktion succ appliziert und liefert dadurch eine Funktion, die als Argument noch eine Liste erwartet. 5 2 Einführung in Haskell 2.1.4 Pattern Matching Mittels Pattern Matchings lässt sich überprüfen, ob ein Ausdruck eine bestimmte Struktur aufweist. fib 0 = 0 fib 1 = 1 fib ( n + 2) = fib n + fib ( n +1) Listing 2.6: Fibonacci mit Pattern Matching Listing 2.6 zeigt Pattern Matching am Beispiel der Fibonacci-Zahlen. Bei einem Aufruf von fib 2 wird zunächst überprüft, ob das Argument auf das Pattern 0 passt. Falls dies, wie in diesem Beispiel, nicht der Fall ist, werden der Reihe nach die restlichen Pattern versucht. Das letzte Pattern (n + 2) passt dabei auf alle Zahlen gröÿer oder gleich zwei, wobei das um zwei verringerte Argument an die Variable n gebunden wird. Um Pattern Matching auch innerhalb von Funktionen benutzen zu können, gibt es case-Ausdrücke. Listing 2.7 zeigt deren Struktur. Je nachdem, auf welches Pattern der Ausdruck exp passt, hat der case-Ausdruck den Wert der Ausdrücke auf der rechten Seite der Pfeile. case exp of pat_1 -> exp_1 ... pat_n -> exp_n Listing 2.7: Pattern Matching mit case-Ausdrücken 2.2 Das Typsystem Haskell ist eine stark getypte Sprache. Das bedeutet, dass der Typ eines jeden Ausdrucks bereits zur Compilezeit feststeht. Aus Sicht des Programmierers bedeutet dies eine gröÿere Sicherheit, aber auch gewisse Einschränkungen. 2.2.1 Datentypen Neben den Basisdatentypen, wie Int, Float und Char, bietet Haskell auch eine Reihe von zusammengesetzten Datentypen. Darunter sind Listen und Tupel, um nur die wichtigsten zu nennen. Auÿerdem haben Benutzer die Möglichkeit, eigene Datentypen zu denieren. 6 2.2 Das Typsystem data IntList = Empty | Cons Int IntList Listing 2.8: Eigener Datentyp IntList IntList deniert. DaEmpty, der die leere Argumente vom Typ Int In Listing 2.8 wird ein neuer Datentyp mit Typkonstruktor bei besteht der Typ IntList entweder aus dem Konstruktor Liste darstellt, oder aus dem Konstruktor bzw. Cons, der zwei IntList erwartet. Durch | getrennt lassen sich weitere Alternativen denieren. Konstruktoren und Typen müssen mit einem Groÿbuchstaben beginnen. Der Typ eines jeden Ausdrucks lässt sich mit :: festlegen. Meistens kann der Com- piler durch Typinferenz den Typ von Ausdrücken selbst bestimmen, in vielen Fällen ist es jedoch von Vorteil, in manchen sogar notwendig, diesen explizit festzulegen. 3 :: Int Empty :: IntList addInt :: Int -> Int -> Int addInt x y = (x :: Int ) + (y :: Int ) :: Int Listing 2.9: Typannotationen Die Beispiele in Listing 2.9 zeigen, wie die Annotation von Typen aussehen kann. Besonders interessant ist dabei die Typannotation in Zeile drei, die den Typ der Funktion addInt in curryzierter Schreibweise angibt. 2.2.2 Polymorphie Funktionen und zusammengesetzte Datentypen lassen sich auch für beliebige Typen denieren. Zum Beispiel möchte man Listen nicht wie in Listing 2.8 für jeden Datentyp einzeln denieren müssen, sondern Listen haben, die beliebige Daten eines beliebigen Typs enthalten können. Auch für die Funktion length, die die Länge einer Liste berechnet, ist es unerheblich, welchen Typ die Elemente der Liste besitzen. List a length length length = Empty | Cons a ( List a ) :: List a -> Int Empty = 0 Cons _ rest = 1 + lenght rest Listing 2.10: Beispiele für Polymorphie Die Denitionen in Listing 2.10 demonstriern den Umgang mit Polymorphie. Das Argument a des Typkonstuktors List ist dabei eine Typvariable, die für einen be- liebigen Typ steht. Typvariablen werden grundsätzlich klein geschrieben. 7 2 Einführung in Haskell 2.2.3 Typklassen Um sowohl das Überladen von Funktionen als auch das Einschränken von polymorphen Funktionen und Datentypen auf Datentypen mit bestimmten Eigenschaften zu ermöglichen, lassen sich Typklassen denieren. Typklassen enthalten Datentypen, auf die bestimmte Funktionen deniert sind. Eine wichtige Klasse ist die Klasse der Datentypen, auf die Gleichheit deniert ist. Diese lässt sich wie in Listing 2.11 denieren. class Eq a where (==) :: a -> a -> Bool Listing 2.11: Typklasse Listing 2.12 zeigt, wie der Datentyp IntList Eq aus 2.8 die Klasse ist so möglich, da die Gleichheit auf dem Datentyp Int Eq instantiiert. Dies bereits deniert ist. instance Eq IntList where Empty == Empty = True ( Cons v1 l1 ) == ( Cons v2 l2 ) = v1 == v2 && l1 == l2 Listing 2.12: Instantiierung der Klasse Eq durch InList Nun lassen sich auch Funktionen denieren, die voraussetzen, dass Gleichheit auf die Argumente deniert ist. Die Funktion solche Funktion. Vor dem => notEq aus 2.13 ist ein Beispiel für eine werden die benötigten Typklassen für die enthaltenen Typvariablen gefordert. notEq :: Eq a = > a -> a -> Bool notEq x y = not ( x == y ) Listing 2.13: Einschränkung von notEq auf Typen der Klasse Eq 2.3 Verzögerte Auswertung Die verzögerte Auswertung, auch Lazy Evaluation, ist eine Strategie zur Auswertung von Ausdrücken, bei der Werte erst dann berechnet werden, wenn sie zur Programmausführung nötig sind. Auf diese Weise lassen sich unnötige Berechnungen vermeiden. Der Ausdruck in Listing 2.14 berechnet die Länge einer Liste mit groÿen FibonacciZahlen. Die einzelnen Elemente der Liste wirklich zu berechnen, würde sehr lange 8 2.4 Monaden dauern, ist aber zur Berechnung der Länge nicht nötig. Durch die verzögerte Auswertung kann dies dann sehr schnell erfolgen. length ( map fib [1000 ,2000 ,3000 ,4000]) Listing 2.14: Länge einer Liste groÿer Fibonacci-Zahlen Ein weiterer Vorteil der verzögerten Auswertung ist die Möglichkeit, mit unendlichen Datenstrukturen arbeiten zu können. Das Beispiel in Listing 2.15 deniert die Liste aller Primzahlen primes. Die Funktion sieve implementiert dabei das Sieb des Eratosthenes, dieses wird auf die unendliche Liste der ganzen Zahlen gewandt, die in Haskell mit [2..] > 1 an- bezeichnet wird. Die erste Zahl in der Liste ist immer eine Primzahl, diese wird also in die Ergebnisliste eingebaut, dann wird rekursiv auf die Restliste angewandt, aus der mit filter sieve alle Vielfachen der ersten Zahl entfernt wurden. sieve :: [ Int ] -> [ Int ] sieve ( x : xs ) = x : sieve ( filter (\ y -> ( y `mod ` x ) /= 0) xs ) primes :: [ Int ] primes = sieve [2..] Listing 2.15: Die unendliche Liste der Primzahlen Mit unendlichen Datenstrukturen kann wie gewohnt gearbeitet werden, es muss nur beachtet werden, dass keine Funktionen verwendet werden, die die Auswertung der gesamten Datenstruktur nötig machen. So würde der Ausdruck length primes nicht terminieren. 2.4 Monaden Die verzögerte Auswertung macht es für den Programmierer schwer vorhersehbar, wann, in welcher Reihenfolge und ob überhaupt bestimmte Ausdrücke ausgewertet werden. Dies ist kein Problem, solange nur rein funktionale Berechnungen durchgeführt werden. Sollen allerdings Ein- und Ausgabeoperationen stattnden, muss die Reihenfolge festgelegt werden können. 2.4.1 Die IO-Monade 2.4.1.1 Ein- und Ausgabe Um Ein- und Ausgabeoperationen zu ermöglichen, wurde in Haskell das Prinzip der monadischen Ein- und Ausgabe eingeführt. Monaden ermöglichen es, Sequenzialisie- 9 2 Einführung in Haskell rung auszudrücken. Für Ein- und Ausgabe gibt es in Haskell den Datentyp IO (Lis- ting 2.16). Jede Funktion, die Einuss auf die Welt, das heiÿt auf Bildschirmausgabe, Dateisystem oder Ähnliches hat, ist von diesem Typ. Listing 2.17 zeigt Funktionen, data IO a = Welt -> (a , Welt ) Listing 2.16: Der Datentyp IO um einzelne Zeichen von der Standardeingabe zu lesen oder auf der Standardausgabe auszugeben. Der Typ () () ist ein Dummy-Typ, zu dem nur ein einziger Wert, ebenfalls geschrieben, gehört. getChar :: IO Char putChar :: Char -> IO () Listing 2.17: Funktionen zur Ein- und Ausgabe von Zeichen Nun fehlen noch Kombinatoren, um IO-Aktionen zu kombinieren, und die Möglichkeit, ohne eigentliche IO-Aktionen auszuführen, Werte in den IO-Kontext zu heben. Hierfür sind die Inxkombinatoren >>=, >> und die Aktion return (Listing 2.18) zuständig. ( > >=) :: IO a -> ( a -> IO b ) -> IO b ( > >) :: IO a -> IO b -> IO b return :: a -> IO a Listing 2.18: IO-Kombinatoren und Die Kombinatoren >>=, return-Aktion >> reichen das Ergebnis der ersten >> nur die Welt weiterreicht und das eigentliche auch bind genannt, und IO-Aktion an die zweite weiter, wobei Ergebnis verwirft. Durch dieses Durchreichen der Welt wird erreicht, dass Aktionen in der vorgesehenen Reihenfolge ausgeführt werden. Jetzt fehlt nur noch die Möglichkeit, IOAktionen auch wirklich auszuführen. Dafür enthält jedes Haskell-Programm eine main-Funktion, die den Programmablauf startet. Listing 2.19 zeigt ein vollständiges Haskell-Programm. Die Aktion echo liest zunächst ein Zeichen ein und gibt es dann sofort wieder aus. Damit das eingelesene Zeichen return als Ergebnis der Aktion zurückgemain ausgeführt, die wiederum echo aufruft. Da der Rückgabewert von main den Typ () haben muss, wird das Ergebnis von echo verworfen und stattdessen der Wert () zurückgegeben. weiter verwendet werden kann, wird es mit geben. Wird das Programm ausgeführt, wird zunächst die Funktion 10 2.4 Monaden echo :: IO Char echo = getChar >>= \ x -> putChar >> return x main :: IO () main = echo >> return () Listing 2.19: Ein einfaches IO-Programm 2.4.1.2 do-Notation Die do-Notation ist syntaktischer Zucker, der ermöglicht, Aktionen ähnlich wie in imperativen Sprachen zu schreiben. Die echo-Aktion aus Listing 2.19 lässt sich dann auch wie in Listing 2.20 schreiben. echo :: IO Char echo = do x <- getChar putChar return x Listing 2.20: Beispiel in do-Notation Dabei werden die Ausdrücke von oben nach unten abgearbeitet. Variablen, die durch var <- IOexp an das Ergebnis eines Ausdrucks gebunden werden, sind im restlichen Code des do-Blocks sichtbar. Auÿerdem lassen sich mit let var = exp auch Ergebnisse von rein funktionalen Berechnungen für den restlichen Block sichtbar machen. Statt Variablen lassen sich in beiden Fällen auch beliebige Pattern verwenden. 2.4.1.3 Exceptions Haskell unterstützt inzwischen auch den Umgang mit Exceptions. So können Exceptions entweder durch fehlerhafte Berechnungen wie die Division durch Null auftreten, aber auch explizit vom Benutzer durch den Aufruf von throw (Listing 2.21) geworfen werden. Das Abfangen von Exceptions stellt jedoch ein Problem dar, denn je nach Ausführungsreihenfolge könnten verschiedene Exceptions auftreten und dadurch zu nicht deterministischem Verhalten führen. Um dieses Problem zu umgehen, ist das Abfangen von Exceptions zum Beispiel durch catch nur innerhalb des IO-Kontexts zulässig, in dem das Programmverhalten auch durch äuÿere Faktoren beeinusst werden kann. Für mehr Details zu diesem Thema siehe A semantics for imprecise exceptions von Peyton Jones et. al [18]. 11 2 Einführung in Haskell throw :: Exception -> a catch :: IO a -> ( Exception -> IO a ) -> IO a Listing 2.21: Funktionen zum Exceptionhandling in Haskell 2.4.1.4 IORef Eine Erweiterung des Haskell 98 Standards, die von den meisten Implementierungen unterstützt wird, sind die IORefs. Da durch IO-Aktionen bildlich gesprochen die Welt verändert wird, spricht auch nichts dagegen, dieser veränderliche Speicherzellen hinzuzufügen. IORefs sind genau diese Speicherzellen. Listing 2.22 zeigt deren Interface. { - Erzeugt eine neue Speicherzelle , die einen Wert enthaelt -} newIORef :: a -> IO ( IORef a ) -- Liest den Wert einer Speicherstelle readIORef :: IORef a -> IO a -- Ueberschreibt den Wert einer Speicherstelle destruktiv writeIORef :: IORef a -> a -> IO () Listing 2.22: Interface von IORef Mit diesen Speicherzellen lassen sich imperative Datenstrukturen realisieren oder auch Informationen in IO-Aktionen hinein kommunizieren, ohne sie explizit zu übergeben. Dies kann zum Beispiel für die GUI-Programmierung interessant sein, um einer Aktion, die die GUI periodisch neu zeichnet, den aktuellen, zu zeichnenden Zustand zu übergeben. 2.4.1.5 unsafePerformIO Die nicht im Haskell 98 Standard enthaltene Funktion es, IO-Aktionen auch auÿerhalb der main-Aktion unsafePerformIO ermöglicht auszuführen. Damit lassen sich, zum Beispiel zur Fehlersuche, Bildschirmausgaben auch innerhalb rein funktionaler Berechnungen erzeugen. Die Funktion trace aus Listing 2.23 gibt eine Zeichenkette aus, sobald der dazugehörige Wert benötigt wird. Häug wird unsafePerformIO auch eingesetzt, um globale Konstanten zu denieglobalCount aus Listing 2.23 ist ein typisches Beispiel für die ren. Die Konstante Implementierung eines globalen Zählers. Da Konstanten von ghc und hugs nur ein einziges Mal berechnet werden, wird bei der ersten Verwendung von globalCount eine neue Speicherzelle erzeugt. Bei späteren Aufrufen wird dieselbe Speicherzelle 12 2.4 Monaden direkt zurückgegeben. Änderungen am Inhalt der Speicherzelle sind dann sofort in allen Programmteilen sichtbar. unsafePerformIO :: IO a -> a trace :: String -> a -> a trace str val = unsafePerformIO ( putStr str >> return val ) globalCount :: IORef Int globalCount = unsafePerformIO ( newIORef 0) Listing 2.23: Verwendung von Wie das unsafe im Namen von unsafePerformIO unsafePerformIO bereits suggeriert, sollte diese Funk- tion nur sehr vorsichtig eingesetzt werden. So geht durch die verzögerte Auswertung die Vorhersehbarkeit der Ausführungsreihenfolge, relativ zur Auch sind Ausdrücke, die main-Aktion, verloren. unsafePerformIO enthalten, nicht mehr referentiell trans- parent, das Ergebnis kann also auch vom Zeitpunkt abhängen, zu dem der Ausdruck ausgewertet wird. 2.4.2 Allgemeine Monaden Der Nutzen von Monaden erschöpft sich nicht bei der Verwendung in der Ein- und Ausgabe, sondern sie lassen sich einsetzen, wann immer eine Sequenzialisierung sinnvoll ist. Eine Monade ist in Haskell ein Typkonstruktor m, der der Klasse Monad (Listing 2.24) angehört. class Monad m where ( > >) :: m a -> m b -> m b ( > >=) :: m a -> ( a -> m b ) -> m b return :: a -> m a fail :: String -> m a Listing 2.24: Die Klasse Monad Wichtig ist dabei nur, dass die folgenden Monadengesetze eingehalten werden: return a >>= f m >>= return (m >>= f) >>= g ⇐⇒ ⇐⇒ ⇐⇒ f a m m >>= (x-> f x >>= g) Ohne hier weiter darauf einzugehen sei angemerkt, dass sowohl die vordenierten Listen als auch der später vorgestellte Transaktionsdatentyp Instanzen der Klasse Monad sind. 13 2 Einführung in Haskell Da die do-Notation wieder in Ausdrücke auf Basis der Kombinatoren (>>) und (>>=) übersetzt wird, lässt sie sich für beliebige Monaden verwenden. 2.5 Speicherverwaltung Die Speicherverwaltung erfolgt bei Haskell automatisch. Der Programmierer muss sich, im Gegensatz zu Sprachen wie zum Beispiel C, nicht selbst um das Allozieren und wieder Freigeben von Speicherbereichen kümmern. Um Speicher wieder freizugeben, überprüft ein Garbage Collector, ob die Datenstrukturen vom ausgeführten Programm noch referenziert werden. Ist dies nicht der Fall, wird die Datenstruktur aus dem Speicher entfernt. 2.6 Das Modulsystem Haskell-Programme lassen sich in einzelne Module gliedern. Module dienen dabei sowohl der logischen Strukturierung eines Programms als auch der Erzeugung von Namensräumen. Durch diese Module wird die Wiederverwendbarkeit des Codes deutlich verbessert. Ein Modul, wie in Listing 2.25 dargestellt, wird mit dem Schlüsselwort module, ge- folgt vom Namen des Moduls, eingeleitet. Dahinter folgt eine optionale Liste der von diesem Modul exportierten Fuktionen, Typen oder Typklassen. Fehlt diese Liste, werden alle Denitionen dieses Moduls exportiert. Nach dem Schlüsselwort folgt eine Reihe von import-Anweisungen, where mit denen einzelne oder auch alle Deni- tionen aus anderen Modulen sichtbar gemacht werden. Importierte Denitionen können durch explizite Anweisung auch wieder exportiert werden. Im restlichen Rumpf benden sich dann die Denitionen. module < name > ( exp1 , ... , exp2 ) where import module_1 ... import module_n -- Begin des Modulrumpfes Listing 2.25: Struktur eines Haskell-Moduls 14 3 Concurrent Haskell Programme, die zum Beispiel über eine grasche Benutzeroberäche Benutzerinteraktion ermöglichen, sollen meist direkt auf Eingaben reagieren können und nicht erst, wenn die aktuelle Berechnung beendet ist. Eine Möglichkeit, dies zu erreichen, ist, das Programm in einer Schleife immer wieder überprüfen zu lassen, ob eine Benutzeraktion verfügbar ist (Abb. 3.1). Dabei muss allerdings sehr darauf geachtet werden, dass Berechnungen während eines Schleifendurchlaufs nicht so lange dauern, dass für den Benutzer unangenehme Verzögerungen entstehen. zeichne GUI mach etwas prüfe auf Eingaben Abbildung 3.1: Programmablauf mit GUI Eine weitere Möglichkeit ist, mit Nebenläugkeit zu arbeiten. Dazu wird das Programm in einzelne Threads aufgeteilt (Abb. 3.2), die dann aus Benutzersicht quasi gleichzeitig ablaufen. Bei dieser zweiten Variante werden zusätzlich Mechanismen benötigt, um Kommunikation zwischen den Threads zu ermöglichen. Für den Entwickler hat dies den Vorteil, dass er sich nicht darum zu kümmern braucht, wie er eine möglicherweise aufwändige Berechnung kurzzeitig unterbricht, um auf eventuelle Benutzereingaben reagieren zu können. Um Nebenläugkeit in Haskell zu ermöglichen, entwickelten Simon Peyton Jones, Andrew Gordon und Sigbjorn Finne Concurrent Haskell [17], das für den ghc und, in ähnlicher Form, auch für hugs implementiert wurde. Soweit nicht anders angegeben, beziehen sich die folgenden Ausführungen auf die ghc-Version. 15 3 Concurrent Haskell Thread 1 Thread 2 zeichne GUI mach etwas Nachrichten prüfe auf Eingaben Abbildung 3.2: Programmablauf mit GUI und Threads 3.1 Threads Jedes Programm startet zunächst mit einem einzelnen Thread, der die main-Aktion abarbeitet. Um einen weiteren Thread zu starten, stellt Concurrent Haskell die Aktion forkIO (Listing 3.1) zur Verfügung. Durch forkIO wird ein neuer Thread ge- startet, der die als Argument übergebene Aktion abarbeitet. Der alte Thread fährt parallel dazu mit der Ausführung seiner Aktion fort. Der Rückgabewert ThreadId dient der eindeutigen Adressierung eines Threads. forkIO :: IO () -> IO ThreadId Listing 3.1: Typ von forkIO Die Reihenfolge, in der IO-Aktionen verschiedener Threads ausgeführt werden, ist nicht festgelegt. Die Implementierung für den ghc wechselt zwischen den Threads an sicheren Punkten (pre-emptive multitasking ), bei hugs geschieht dies nur, wenn der aktuell ausgeführte Thread blockiert oder der Wechsel durch den Aufruf von yield erzwungen wird (co-operative multitasking ). 3.2 Kommunikation In den meisten Anwendungsszenarien müssen Threads Informationen austauschen oder den Zugri auf gemeinsame Ressourcen synchronisieren. Für die dafür notwendige Kommunikation gibt es bei Concurrent Haskell den Datentyp sind den bereits vorgestellten IORefs Als zusätzliche Eigenschaften haben und Threads können auf eine Operationen auf 16 MVars. MVar. Diese ähnliche, destruktiv änderbare Speicherzellen. MVars jedoch zwei Zustände, leer und gefüllt, MVar suspendieren. Listing 3.2 zeigt die grundlegenden 3.3 Dinierende Philosophen in Concurrent Haskell newEmptyMVar :: IO ( MVar a ) newMVar :: a -> IO ( MVar a ) putMVar :: MVar a -> a -> IO () takeMVar :: MVar a -> IO a isEmptyMVar :: MVar a -> IO Bool readMVar :: MVar a -> IO a Listing 3.2: Operationen auf Speicherzellen werden mit newEmptyMVar bzw. MVars newMVar erzeugt. Mit putMVar wird ein Wert in eine leere Speicherzelle geschrieben. Falls diese jedoch voll ist, suspendiert der Thread, bis sie wieder leer ist. Das Gegenstück dazu ist takeMVar, die den Wert einer gefüllten Speicherzelle liest und sie leer hinterlässt. Auf eine leere MVar angewandt, suspendiert der Thread auch dabei. Auÿerdem gibt es eine Aktion, die testet, ob eine MVar leer ist (isEmptyMVar), und die Möglichkeit, den Wert einer MVar zu lesen, ohne sie zu leeren (readMVar). Bei all diesen Aktionen ist gewährleistet, dass sie atomar ausgeführt werden. Dies gilt jedoch nicht für zusammengesetzte Aktionen. Sind mehrere Threads auf eine MVar suspendiert, um beispielsweise einen Wert zu lesen, wird, nachdem ein weite- rer Thread einen Wert geschrieben hat, nicht-deterministisch einer der wartenden Threads ausgewählt, der dann mit der Programmausführung fortfahren kann. MVars existieren weitere Kommunikationsabstraktionen: eine überMVar-Variante SampleVar, eine Nachrichten-Warteschlange Chan sowie von Semaphoren QSem und QSemN. Auf der Basis von schreibbare zwei Arten 3.3 Dinierende Philosophen in Concurrent Haskell Das Problem der dinierenden Philosophen ist ein bekanntes Beispiel zur Veranschaulichung eines Problems, das die exclusive Nutzung von gemeinsamen Ressourcen in nebenläugen Programmen mit sich bringen kann. Wie in Abb. 3.3 dargestellt, sitzen in diesem Beispiel fünf Philosophen um einen runden Tisch. Jeder Philosoph tut abwechselnd zwei Dinge: Essen und Denken. Letzteres geschieht ohne Hilfsmittel, zum Essen jedoch steht in der Mitte des Tisches ein Teller mit Nahrung. Auÿerdem liegt zwischen den Philosophen jeweils ein Essstäbchen, das jeweils von zwei nebeneinandersitzenden Philosophen geteilt werden muss. Um zu essen, benötigt ein Philosoph zwei Stäbchen. Dazu nimmt er sich zunächst das linke und dann das rechte Stäbchen. Nach dem Essen legt er die beiden Stäbchen wieder zurück. Das Ganze geht solange gut, wie die Philosophen nicht alle gleichzeitig beschlieÿen, mit dem Essen zu beginnen. In diesem Fall nämlich würden alle Philosophen zuerst ihr linkes Stäbchen greifen und damit ihrem Nebensitzer sein rechtes Stäbchen wegnehmen. Ein rechtes Stäbchen ist dann für keinen der Philoso- 17 3 Concurrent Haskell phen mehr verfügbar, sie können also nicht mit dem Essen beginnen und verhungern schlieÿlich. Abbildung 3.3: Das Problem der dinierenden Philosophen Mit den Mitteln, die Concurrent Haskell zur Verfügung stellt, lässt sich nun das Problem der dinierenden Philosophen wie in Listing 3.3 implementieren. Die Essstäbchen werden von dem abstrakten Datentyp eine MVar implementiert ist. Ist die MVar Stick repräsentiert, der durch gefüllt, liegt das Stäbchen auf dem Tisch, ist sie leer, ist es gerade in Benutzung. Stäbchen werden mit Mit getStick newStick erzeugt und liegen zunächst auf dem Tisch bereit. releaseStick MVars garantiert dabei, dass kann ein Stäbchen vom Tisch genommen werden, mit wird es wieder zurückgelegt. Die Implementierung durch die Aktionen atomar ausgeführt werden. Zunächst erzeugt der Hauptthread eine Reihe von Stäbchen. Daraufhin werden die die Philosophen darstellenden Threads erzeugt, denen je zwei nebeneinander liegende Sticks übergeben werden. Bildlich gesprochen werden die Philosophen zwischen zwei Stäbchen gesetzt. Sind, bis auf einen, alle Plätze besetzt, nimmt der Haupthread selbst diesen Platz an der Philosophentafel ein, indem er die Aktion Ein Philosoph nimmt zuerst mit getStick phil aufruft. das aus seiner Sicht linke Stäbchen vom Tisch, danach das rechte. Nun kann er essen. Ist er damit fertig, legt er die Stäbchen nacheinander wieder auf den Tisch, um sich seiner Hauptaufgabe, dem Denken, zu widmen. Dieser Ablauf wird dann unbeschränkt wiederholt. Ausgehend von einem Szenario, bei dem alle Stäbchen auf dem Tisch liegen, ist leicht einzusehen, dass, sollten alle Threads genau so weit ausgeführt werden, bis sie ihr 18 3.3 Dinierende Philosophen in Concurrent Haskell type Stick = MVar () newStick :: IO Stick newStick = newMVar () getStick :: Stick -> IO () getStick stick = takeMVar stick releaseStick :: Stick -> IO () releaseStick stick = putMVar stick () main :: IO () main = do stick0 <- newStick stick1 <- newStick stick2 <- newStick stick3 <- newStick stick4 <- newStick forkIO forkIO forkIO forkIO ( phil ( phil ( phil ( phil stick1 stick2 stick3 stick4 stick2 ) stick3 ) stick4 ) stick0 ) phil stick0 stick1 phil :: Stick -> Stick -> IO () phil left right = do getStick left getStick right -- eat releaseStick left releaseStick right -- think phil left right Listing 3.3: Dinierende Philosophen in Concurrent Haskell 19 3 Concurrent Haskell linkes Stäbchen genommen haben, kein Stäbchen mehr auf dem Tisch liegt. Durch die blockierende Wirkung von takeMVar, angewandt auf leere MVars, ist dann kein weiterer Fortschritt mehr möglich. Die Kombination von einzeln richtiger Aktionen kann also zu einem Problem führen. Eine Möglichkeit, dieses Deadlock zu verhindern, ist, nur einem Philosophen zur Zeit das Aufnehmen von Stäbchen vom Tisch zu erlauben. Dies kann durch eine zusätzliche MVar erreicht werden, die als binäre Semaphore fungiert. Listing 3.4 zeigt eine Implementierung dieses Ansatzes. globalLock :: MVar () globalLock = unsafePerformIO ( newMVar ()) phil :: MVar () -> MVar () -> IO () phil left right = do takeMVar globalLock getStick left getStick right putMVar globalLock -- eat releaseStick left releaseStick right -- think phil left right Listing 3.4: Dinierende Philosophen mit globalem Lock Der gegenseitige Ausschluss wird hier mit einer Konstanten dadurch muss die globalLock main-Aktion aus Listing 3.3 nicht verändert werden. MVar vom Hauptthread erzeugen zu lassen und dings auch möglich, die ermöglicht, Es ist allersie an jeden Philosophen zu übergeben. Zwar kann bei dieser Lösung kein Deadlock mehr auftreten, allerdings hat sie auch gravierende Nachteile. Zum Beispiel können auch einander gegenübersitzende Philosophen, die sich kein Stäbchen teilen müssen, die Stäbchen nur nacheinander vom Tisch nehmen. Zu noch gröÿeren Verzögerungen kann es kommen, wenn ein Philosoph, der neben einem bereits Essenden sitzt, das Recht bekommt, seine Stäbchen zu nehmen. Da eines der Stäbchen dann nicht verfügbar ist, muss er warten, bis sein Nachbar mit dem Essen fertig ist. Solange kann allerdings auch kein anderer Philosoph zu essen beginnen, da das Recht, die Stäbchen zu nehmen, noch vom wartenden Philosophen blockiert wird. Das Erkennen und Beheben von derartigen Problemen setzt oft die Kenntnis der Implementierung voraus, dadurch werden eingeführte Abstraktionen zunichte gemacht. 20 4 Transaktionen in Haskell Nebenläuge Programme sind schwer zu entwickeln, häug ist es nötig, dass ein Thread exklusiven Zugri auf Ressourcen erhält. Der Lock-basierte Ansatz aus Abschnitt 3.3 hat einige Nachteile. So kann das Kombinieren von einzelnen, korrekten Aktionen zu Fehlern führen, Abstraktionsebenen gehen verloren und das Vermeiden von Problemen durch das Sperren von gröÿeren Codeabschnitten führt zu unnötiger Sequenzialisierung. Um diesen Problemen zu begegnen, entwickelten Tim Harris, Simon Marlow, Simon Peyton Jones und Maurice Herlihy [8] ein Konzept und eine Implementierung für transaktionsbasierte Kommunikation für den ghc: Die STMBibliothek. Die Idee dabei ist, dass Aktionen, die atomar ausgeführt werden sollen, zu einem Block zusammengefasst werden und die atomare Ausführung aller Aktionen innerhalb dieses Blocks garantiert wird. Dabei wird kein Lock verwendet, das gewährleistet, dass nur ein Thread zur Zeit einen atomaren Block ausführen kann, sondern jeder Thread geht zuerst einmal optimistisch vor und führt seine Aktionen aus. Schreibund Lesezugrie auf den Speicher werden dabei protokolliert. Am Ende des atomaren Blocks wird überprüft, ob die Sicht des Threads auf den Speicher konsistent war. Ist dies der Fall, wird die Aktion abgeschlossen, wenn nicht, werden die Änderungen rückgängig gemacht und der Block erneut ausgeführt. Ein solcher Block wird als Transaktion bezeichnet. Da Transaktionen unter Umständen wieder rückgängig gemacht werden, wird auch deutlich, dass sie keine Seiteneekte haben dürfen. Eine Anweisung, die zum Beispiel einen Druckauftrag absendet, kann nicht rückgängig gemacht werden. Auÿerdem werden Transaktionen nach einem Abbruch erneut ausgeführt, unter diesen Umständen könnte ein Druckauftrag auch mehrmals abgesandt werden. 4.1 Original-Bibliothek In Haskell wurde für Transaktionen der Datentyp STM a eingeführt. Jede Transaktion muss diesen Typ besitzen. Dadurch wird auch garantiert, dass keine Seiteneekte stattnden können, da diese den Datentyp IO besitzen. Ausgeführt werden Transaktionen von der Aktion atomically (Listing 4.1), der eine Transaktion übergeben wird, die sie dann in der IO-Monade ausführt. 21 4 Transaktionen in Haskell atomically :: STM a -> IO a Listing 4.1: Typ von atomically 4.1.1 Grundlegende Transaktionen Zugrie auf Speicherzellen, wie MVar und IORef, sind nur innerhalb der IO-Monade möglich. Da aber IO-Aktionen innerhalb von Transaktionen nicht erlaubt sind, dienen TVars, Transaction Variables ( Listing 4.2) der Kommunikation zwischen Threads. newTVar :: a -> STM ( TVar a ) writeTVar :: TVar a -> a -> STM () readTVar :: TVar a -> STM a Listing 4.2: Operationen auf TVars TVars sind zwar ähnlich wie MVars veränderbare Speicherzellen, allerdings gibt es entscheidende Unterschiede. So können TVars nicht leer sein, auÿerdem werden Threads beim Schreiben oder Lesen nicht blockiert. Die Operationen newTVar, writeTVar und readTVar sind dabei die Grundbausteine für Transaktionen. 4.1.2 Sequenzielle Komposition Wie der Datentyp Monad IO ist auch der Transaktionsdatentyp STM eine Instanz der Klasse (siehe S. 13). Dadurch ist es möglich, Transaktionen sequenziell zu kombinie- ren. Natürlich lässt sich auch die do-Notation verwenden. Listing 4.3 zeigt eine Transaktion, die einen neuen Wert in eine den alten Wert zurückgibt. Sollte zwischen dem Lesen der TVar TVar schreibt und und dem Schreiben des neuen Wertes ein anderer Thread eine Transakton beenden, die den Wert derselben Variablen ändert, so wird dies am Ende der Transaktion festgestellt und die Transaktion wird neu begonnen. swapTVar :: TVar a -> a -> STM a swapTVar t new = do old <- readTVar t writeTVar t val return old Listing 4.3: Beispiel für eine zusammengesetzte Transaktion 22 swapTVar 4.1 Original-Bibliothek 4.1.3 Blockieren von Transaktionen Häug ist es nötig, den exklusiven Zugri eines Threads auf externe Ressourcen, zum Beispiel Dateien, zu gewährleisten. Da Ein- und Ausgabeoperationen innerhalb von Transaktionen nicht möglich sind, wird ein Mechanismus benötigt, der Threads vorübergehend blockieren kann. Dafür wurde die Aktion retry eingeführt. Sie wird explizit vom Programmierer aufgerufen, um einen Thread solange zu blockieren, bis eine Ausführung der Transaktion ohne Aufruf von 4.4 zeigt, wie retry retry möglich ist. Die Transaktion genutzt werden kann, um exklusiven Zugri auf eine Datei zu erhalten. type FileLock = TVar Bool newFileLock :: STM FileLock newFileLock = newTVar True getFile :: FileLock -> STM () getFile lock = do avail <- readTVar lock if avail then ( writeTVar lock False ) else retry Listing 4.4: Demonstration von retry um exklusiven Zugri auf eine Datei zu erhal- ten Wird retry aufgerufen, so werden jegliche Schreibaktionen der Transaktion verwor- fen und die Transaktion wird von vorne begonnen. Dies geschieht allerdings nicht durch inezientes Busy Waiting. Während der Transaktion wird darüber Protokoll geführt, welche Variablen bis zum retry gelesen wurden. Ein anderer Ausführungs- pfad ist erst dann möglich, wenn eine der Variablen geändert wurde. Bis dies geschieht, wird der Thread blockiert. Wurde mindestens eine Variable geändert, wird die Ausführung der Transaktion erneut versucht. Wird genau eine Datei durch atomically (getFile lock) gesperrt, so ist dies äqui- valent zum Lock-basierten Ansatz. Dies ändert sich jedoch, wenn ein Thread exklusiven Zugri auf mehr als eine Datei benötigt. Mit dem Ausdruck in Listing 4.5 kann sich ein Thread den Zugri auf zwei Dateien sichern. Dies führt auch dann nicht zu Problemen, wenn ein anderer Thread die gleichen Aktionen in umgekehrter Reihenfolge ausführt. Im Lock-basierten Ansatz könnte dies zu einem Deadlock führen. Auch das Problem der dinierenden Philosophen, aus Listing 3.3, lässt sich nun mithilfe von Transaktionen implementieren. Ein Stäbchen wird dabei als Wert vom Typ Bool TVar, die einen enthält, realisiert. Da wie in Listing 4.6 zu sehen ist, werden beide Stäbchen innerhalb einer Transaktion vom Tisch genommen. Ein Deadlock kann daher, wie bei der Implementierung mit globalem Lock in Listing 3.4, nicht 23 4 Transaktionen in Haskell atomically ( do getFile lock1 getFile lock2 ) Listing 4.5: Exklusiver Zugri auf zwei Dateien auftreten. Die dort beschriebenen Probleme, dass Philosophen immer nur nacheinander das Recht bekommen, ein Stäbchen von Tisch zu nehmen und in bestimmten Fällen unnötigerweise lange aufeinander warten müssen, gibt es hier jedoch nicht. phil left right = do atomically ( do takeStick left takeStick right ) atomically ( do putStick left putStick right ) phil left right takeStick :: TVar Bool -> STM () takeStick stick = do avail <- readTVar stick if avail then writeTVar stick False else retry Listing 4.6: Dinierende Philosophen mit Transaktionen 4.1.4 Alternative Komposition Zusätzlich zur sequenziellen Komposition, bei der die Transaktionen hintereinander ausgeführt werden, ermöglicht die STM-Bibliothek auch, Transaktionen als Alternativen zu kombinieren. Dies ermöglicht die Funktion orElse (Listing 4.7). Diese be- orElse :: STM a -> STM a -> STM a Listing 4.7: Typ von orElse kommt zwei Transaktionen übergeben und führt zunächst die erste davon aus. Wird die erste Transaktion erfolgreich abgeschlossen, so ist deren Ergebnis auch das Er- orElse-Ausdrucks. Führt die erste Transaktion allerdings zur Ausführung retry, so werden deren Schreibaktionen verworfen und die zweite Transaktion gebnis des von wird ausgeführt. Durch orElse kann nun, wie in Listing 4.8, der exklusive Zugri auf eine von zwei Dateien ermöglicht werden. 24 4.1 Original-Bibliothek atomically ( orElse ( do getFile lock1 return file1 ) ( do getFile lock2 return file2 )) Listing 4.8: Alternativer Zugri auf zwei Dateien Der Entwickler muss dabei nicht wissen, an welche Bedingung die erfolgreiche Ausführung von getFile geknüpft ist. Die Abstraktionsebene bleibt erhalten. 4.1.5 Exceptions Ähnlich wie in der IO-Monade ist Exceptionhandling auch in Transaktionen möglich. Dafür gibt es die Anweisungen in Listing 4.9. throw :: Exception -> a catchSTM :: STM a -> ( Exception -> STM a ) -> STM a Listing 4.9: Exceptionhandling in Transaktionen throw ist dabei die überall einsetzbare, polymorphe Funktion aus dem Control.Exception. Die Funktion catchSTM ermöglicht es, auch innerhalb Die Anweisung Modul von Transaktionen Exceptions abzufangen. Dies ist sonst nur im IO-Kontext möglich. Da Exceptions normalerweise schwerwiegende Fehler anzeigen, werden Transaktionen bei deren Auftreten abgebrochen, Schreibaktionen werden nicht ausgeführt. Wird eine Exception noch innerhalb einer Transaktion durch werden nur die Schreibaktionen des ersten Argumentes catchSTM abgefangen, so 1 von catchSTM verworfen . Wird eine Exception nicht abgefangen, so wird sie weiter nach auÿen propagiert. 4.1.6 Invarianten In Version 6.8 des ghc wurde die STM-Bibliothek um ein von Tim Harris und Simon Peyton Jones [9] entwickeltes Konzept erweitert, das es ermöglicht, Invarianten für Transaktionen zu formulieren. Dazu wurden zwei neue Funktionen (Listing 4.10) eingeführt. Das Argument der beiden Funktionen ist dabei die zu testende Invariante. Diese Invariante wird sowohl an der Stelle getestet, an der sie steht, als auch am Ende dieser und aller nachfolgenden Transaktionen. 1 Dieses Verhalten wurde geändert. Ursprünglich wurden auch die Schreibaktionen des ersten Argumentes von catchSTM ausgeführt, wenn die Ausführung des zweiten erfolgreich war. 25 4 Transaktionen in Haskell alwaysSucceeds :: STM a -> STM () always :: STM Bool -> STM () Listing 4.10: Erzeugung von Invarianten in Transaktionen Der einzige Unterschied zwischen den beiden Funktionen ist, dass der Test der Invariante bei always alwaysSucceeds nur dann fehlschlägt, wenn sie eine Exception wirft. Bei geschieht dies auch dann, wenn das Ergebnis der Invariante False ist. Natürlich wäre es ziemlich aufwändig, am Ende einer Transaktion tatsächlich alle im Programmverlauf erzeugten Invarianten zu überprüfen. Aus diesem Grund wird für jede Invariante protokolliert, welche Variablen sie bei ihrer letzten Überprüfung gelesen hat. Nur wenn eine dieser Variablen während einer Transaktion geändert wurde, muss die Invariante erneut getestet werden. Invarianten können sämtliche in Transaktionen zulässigen Aktionen enthalten, also auch Schreibaktionen, retry oder selbst wieder alwaysSucceeds und always. Es ist nicht ganz klar, wie sich Invarianten in diesen Fällen sinnvollerweise verhalten sollten. Aus diesem Grund wurden die folgenden Entscheidungen getroen: • Invarianten können Schreibaktionen durchführen, diese bleiben jedoch nur innerhalb der Invariante sichtbar und werden am Ende der Überprüfung verworfen. • Sollte eine Invariante retry ausführen, so wird die gesamte Transaktion abge- brochen und neu gestartet. • Wenn eine Invariante selbst wieder alwaysSucceeds oder always aufruft, wird die darin enthaltene Invariante direkt überprüft, allerdings wird sie am Ende der aktuellen und aller folgenden Transaktionen nicht als eigenständige Invariante behandelt. Ein Anwendungsbeispiel für Invarianten ist in Listing 4.11 zu sehen. Hier wird eine TVar deniert, die Werte vom Typ Int bis zu einer bestimmten Grenze enthalten kann. Diese Grenze, oder besser gesagt die Invariante, die diese Grenze garantiert, wird dabei schon bei der Erzeugung einer LimitedTVar mit dieser assoziiert. Am Ende jeder Transaktion, die sie von nun an zum Beispiel durch incLimitedTVar verändert, wird die Invariante überprüft und gegebenenfalls eine Exception geworfen. 4.2 Lightweight-Bibliothek für Transaktionen in Haskell Die original STM-Bibliothek ist durch externen C-Code für den ghc implementiert. Sie ist für andere Implementierungen, die Concurrent Haskell unterstützen, wie zum 26 4.2 Lightweight-Bibliothek für Transaktionen in Haskell type LimitedTVar = TVar Int newLimitedTVar :: Int -> STM LimitedTVar newLimitedTVar lim = do tv <- newTVar 0 always ( do val <- readTVar tv return ( val <= lim )) return tv incLimitedTVar :: Int -> LimitedTVar -> STM () incLimitedTVar delta tv = do val <- readTVar tv writeTVar tv ( val + delta ) Listing 4.11: Invarianten-Beispiel: LimitedTVar Beispiel hugs, nicht verwendbar. Aus diesem Grund haben Frank Huch und Frank Kupke eine rein in Haskell implementierte Bibliothek für Transaktionen entwickelt [13]. Bei der hier vorgestellten Implementierung handelt es sich um eine leicht modizierte Version dieser Bibliothek, die auch Grundlage der Erweiterung des Concurrent Haskell Debuggers ist. STM. Demnach ist eine STM-Aktion eine IO-Aktion, StmState erhält und ein Ergebnis vom Typ STMResult Listing 4.12 zeigt den Datentyp die einen Zustand vom Typ liefert. Im übergebenen Zustand werden für die Ausführung der Aktion wichtige Informationen gesammelt. Das Ergebnis zeigt an, ob eine Aktion gelungen ist oder ob sie aus irgendeinem Grund abgebrochen wurde. Auÿerdem enthält das Ergebnis gegebenenfalls einen neuen Zustand und im Erfolgsfall das berechnete Ergebnis. Werden Aktionen durch den (>>=)-Operator hintereinander ausgeführt, werden im Erfolgsfall der neue Zustand und das berechnete Ergebnis an die nachfolgende STMAktion weitergereicht. Alle anderen Ergebnisse werden nach oben weitergereicht. 4.2.1 TVars Da der Inhalt von Transaktionsvariablen erst am Ende einer kompletten Transaktion verändert werden kann, ist es notwendig, die Schreibaktionen zu sammeln. Da verschiedene Variablen im Allgemeinen auch unterschiedliche Typen besitzen, ist es durch Haskells Typsystem nicht möglich, diese in einer Datenstruktur vorzuhalten. Daher werden Schreibaktionen als IO-Aktion im Transaktionszustand gepuert. Dies wirft jedoch das Problem auf, wie der Wert einer während der Transaktion bereits geschriebenen TVar wieder gelesen werden kann. Dazu wird während der Transakti- onsausführung zusätzlich zur commit -Aktion eine restore -Aktion gesammelt, die die Schreibaktionen der commit -Aktion wieder rückgängig machen kann. Listing 4.13 zeigt, wie dies implementiert ist. 27 4 Transaktionen in Haskell data STM a = STM ( StmState -> IO ( STMResult a )) instance Monad STM where ( STM tr1 ) > >= k = STM (\ state -> do stmRes <- tr1 state case stmRes of Success newState a -> let ( STM tr2 ) = k a in catch ( tr2 newState ) (\ e -> return ( Exception newState e )) Retry newState -> return ( Retry newState ) Exception newState e -> return ( Exception newState e ) ) return x = STM (\ state -> return ( Success state x )) data STMResult a = Retry StmState | Success StmState a | Exception StmState Exception Listing 4.12: Die STM Monade data StmState = TST { writtenTVars :: [ ID ] , commit :: IO () , restore :: IO ()} data TVar a = TVar ( MVar ( IORef a )) ID writeTVar :: TVar a -> a -> STM () writeTVar ( TVar tVarRef id ) val = STM (\ stmState -> do tVarVal <- readMVar tVarRef let newState = stmState { writtenTVars = id : writtenTVars stmState , commit = ( do commit stmState newTVarVal <- newIORef v takeMVar tVarRef putMVar tVarRef newTVarVal ) , restore = ( do takeMVar tVarRf putMVar tVarRef tVarVal restore stmState )} return ( Success newState ())) Listing 4.13: Schreiben von 28 TVars 4.2 Lightweight-Bibliothek für Transaktionen in Haskell Um nun eine bereits geschriebene Variable zu lesen, werden zuerst die Schreibaktionen ausgeführt, die Variable wird gelesen und danach werden die Schreibaktionen wieder rückgängig gemacht. Damit kein anderer Thread in der Zwischenzeit die TVar verändert, wird die ganze Aktion durch ein globales Lock geschützt. Der ganze Vorgang ist zwar sehr aufwändig, allerdings sollte dieser Fall in realen Anwendungen nur recht selten auftreten. Die Implementierung ist Listing 4.14 zu entnehmen. readTVar :: TVar a -> STM a readTVar ( TVar tVarRef id ) = STM (\ stmState -> if ( elem id ( writtenTVars stmState )) then do takeGlobalLock commit stmState tVarVal <- readMVar tVarRef val <- readIORef tVarVal restore stmState freeGlobalLock return ( Success stmState val ) else do tVarVal <- readMVar tVarRef val <- readIORef tVarVal return ( Success stmState val )) Listing 4.14: Lesen von Beim Erzeugen von neuen TVars TVars (Listing 4.15) muss im Gegensatz zum Lesen und Schreiben auf kaum etwas geachtet werden. Neue Variablen können für andere Threads vor dem Beenden der Transaktion nicht sichtbar sein. Wird eine Transaktion abgebrochen, nachdem sie eine neue Variable erzeugt hat, so existieren auf diese keine Verweise mehr und der Garbage Collector erledigt den Rest. newTVar :: a -> STM ( TVar a ) newTVar v = STM (\ stmState -> do id <- getGlobalId newTVarVal <- newIORef v newTVarRef <- newMVar newTVarVal let tVar = ( TVar newTVarRef id ) return ( Success stmState tVar )) Listing 4.15: Erzeugen einer neuen TVar 4.2.2 Konsistenzprüfung und Ausführung Dem Leser mag aufgefallen sein, dass der Typ MVar (IORef a) von Einträgen in Transaktionsvariablen für die bisherigen Funktionen ein wenig zu kompliziert ist. 29 4 Transaktionen in Haskell Der Grund dafür ist, dass bevor die commit-Aktion ausgeführt werden kann, eine Konsistenzprüfung durchgeführt werden muss. Dies geschieht, indem geprüft wird, ob die Werte, die aus den Variablen gelesen wurden, noch mit den aktuellen übereinstimmen. Da Gleichheit nicht für alle Datentypen deniert ist, geschieht dies durch Vergleich der Wie die IORef, die bei jedem Schreibvorgang neu erzeugt wird. commit-Aktion, wird auch die Konsistenzprüfung als IO-Aktion im Trans- aktionszustand gesammelt. Listing 4.16 zeigt die notwendigen Änderungen. data StmState = TST { ... , isValid :: IO Bool } readTVar ( TVar tVarRef id ) = STM (\ stmState -> if ( elem id ( writtenTVars stmState )) then do ... else do tVarVal <- readMVar tVarRef val <- readIORef tVarVal let newState = stmState { isValid = do b <- isValid stmState if b then do newTVarVal <- readMVar tVarRef return ( tVarVal == newTVarVal ) else return False return ( Success stmState val )) Listing 4.16: Konsistenzprüfung Nun ist es möglich, Transaktionen mit eingeschränktem Funktionsumfang durch die Aktion startSTM atomically (Listing 4.17) auszuführen. Dazu wird der Transaktion mit ein initialer Zustand übergeben. Für jedes Ergebnis wird überprüft, ob die Sicht auf die Variablen konsistent war. Ist dies nicht der Fall, werden alle Änderungen verworfen und die Transaktion neu gestartet. War der Durchlauf valide, so werden Exceptions nach auÿen weitergegeben oder im Erfolgsfall die Änderungen durchgeführt und das berechnete Ergebnis zurückgegeben. Damit währenddessen kein anderer Thread die Variablen ändert, wird die Überprüfung der Konsistenz und das Ausführen der commit-Aktion durch das bereits bei readTVar (Listing 4.14) verwendete globale Lock geschützt. 4.2.3 retry Wenn die Aktion retry ausgeführt wird, so soll die gesamte Transaktion abgebrochen und solange gewartet werden, bis sich eine der während der Ausführung gelesenen Variablen geändert hat. Dazu wird am Anfang jeder Transaktion eine neue 30 MVar 4.2 Lightweight-Bibliothek für Transaktionen in Haskell atomically :: STM a -> IO a atomically stmAction = do stmResult <- startSTM stmAction case stmResult of Success newState res -> do takeGlobalLock valid <- isValid newState if valid then do commit newState freeGlobalLock return res else do freeGlobalLock atomically stmAction Exception newState e -> do takeGlobalLock valid <- isValid newState freeGlobalLock if valid then throw e else atomically stmAction Listing 4.17: Implementierung von atomically erzeugt und im Transaktionszustand in einem neuen Feld Auf diese MVar retryMVar vorgehalten. soll der Thread suspendieren, bis eine Änderung eingetreten ist. Dazu muss ein Thread, der eine Variable ändert, allerdings wissen, welche Threads er wieder aufwecken soll. Zu diesem Zweck erhält jede TVar noch eine Liste mit MVars, in die sich die wartenden Threads eintragen können. Während der Ausführung sammeln die Threads nun zwei zusätzliche Aktionen an, wait und notify. In wait wird bei Leseaktionen eine IO-Aktion angesammelt, die die retryMVar in die Liste einträgt. Diese wird im Fall eines Aufrufs von retry ausgeführt. Bei Schreibaktionen wird in notify eine Aktion gesammelt, die die Threads, die auf die Änderung der Variablen warten, aufweckt. Einige Details zu den Änderungen, die dazu notwendig sind, können Listing 4.18 entnommen werden. 4.2.4 orElse retry-Aktion nichts weiter macht als Retry als Ergebnis zu liefern, ist es recht orElse zu implementieren. Dazu muss orElse lediglich überprüfen, welches Ergebnis die erste Transaktion liefert. Ist dies ein Retry, werden sämtliche Schreibaktionen verworfen. Dazu müssen lediglich die Felder writtenTVars, commit, restore und notify auf ihren ursprünglichen Wert zurückgesetzt werden. Die Werte in wait und isValid dagegen müssen erhalten Da die unkompliziert, die alternative Komposition mit bleiben. Dann wird die zweite Transaktion ausgeführt. Wird als Ergebnis Success oder Exception zurückgegeben, so wird die zweite Trans- 31 4 Transaktionen in Haskell data TVar = TVar ( MVar ( IORef a )) ID ( MVar [ MVar ()]) retry :: STM a retry = STM (\ stmState -> return ( Retry stmState )) atomically stmAction = do stmResult <- startSTM stmAction case stmResult of Success newState res -> do ... if valid then do commit newState notify newState freeGlobalLock return res else do ... Exception newState e -> ... Retry newState -> do takeGlobalLock valid <- isValid newState if valid then do wait newState freeGlobalLock takeMVar ( retryMVar newState ) atomically stmAction else do freeGlobalLock atomically stmAction Listing 4.18: Änderungen zur Einführung von 32 retry 4.2 Lightweight-Bibliothek für Transaktionen in Haskell aktion nicht ausgeführt, und das Ergebnis nach oben weitergegeben. orElse :: STM a -> STM a -> STM a orElse ( STM stm1 ) ( STM stm2 ) = STM (\ stmState@TST { writtenTVars = fWritten , commit = fCommit , restore = fRestore , notify = fNotify } -> do stm1Res <- stm1 stmState case stm1Res of Retry newState -> let newState ' = newState { writtenTVars = fWritten commit = fCommit , restore = fRestore , notify = fNotify } catch ( stm2 newState ') (\ e -> return ( Exception newState ' e )) _ -> return stm1Res Listing 4.19: Implementierung von orElse 4.2.5 Exceptions Da throw polymorph ist, lässt es sich überall, also auch in Transaktionen einsetzen. Damit bleibt nur zu klären, wie Art orElse, catchSTM funktioniert. Im Prinzip ist catchSTM eine Retry beim Auftreten einer Exception eine das anstatt beim Ergebnis zweite Transaktion ausführt. Entsprechend ähnlich sind sich auch die Implementierungen. Schon bei der sequenziellen Komposition und bei orElse wurden Excep- tions so abgefangen, dass sie für eine aussagekräftige Konsistenzprüfung am Ende frühestmöglich in Exceptions vom Typ STMResult umgewandelt werden und damit die im Transaktionszustand gesammelten Informationen erhalten bleiben. Exceptions werden dadurch nur dann nach auÿen durchgereicht, wenn sie nicht aufgrund einer inkonsistenten Sicht auf den Speicher zustandegekommen sind. 4.2.6 Bekannte Nachteile Die hier vorgestellte Lightweight-Bibliothek hat neben den bereits erwähnten Vorteilen gegenüber der Original-Bibliothek zwei bekannte Nachteile. Zum einen ist sie bis zu sechsmal langsamer. Dies sollte jedoch in realen Anwendungen keine sehr groÿe Rolle spielen, da sie wohl kaum ausschlieÿlich Transaktionen ausführen werden. Zum anderen kann, wie in [8] beschrieben, eine inkonsistente Sicht auf Transaktionsvariablen dazu führen, dass eine Transaktion nicht terminiert. Dort wurde das Problem behoben, indem jedesmal, wenn der Scheduler zu einem Thread wechselt, der gerade eine Transaktion ausführt, ein Validitätstest durchgeführt wird. In der 33 4 Transaktionen in Haskell catchSTM :: STM a -> ( Exception -> STM a ) -> STM a catchSTM ( STM stm1 ) eHandler = STM (\ stmState@TST { writtenTVars = fWritten , commit = fCommit , restore = fRestore , notify = fNotify } -> do res <- catch ( stm1 stmState ) (\ e -> return ( Exception stmState e )) case res of Exception newState e -> do let ( STM stm2 ) = eHandler e let newState ' = newState { writtenTVars = fWritten commit = fCommit , restore = fRestore , notify = fNotify } catch ( stm2 newState ') (\ e -> return ( Exception newState ' e )) _ -> return res Listing 4.20: Implementierung von catchSTM hier vorgestellten Implementierung ist dies jedoch nicht möglich. Eine Möglichkeit wäre, bei jeder Leseaktion auf Konsistenz zu testen. Dies wäre jedoch auch recht zeitaufwändig. Eine andere Möglichkeit wurde von Frank Huch vorgeschlagen. Dabei trägt sich jeder Thread beim Lesen in eine zur TVar gehörende Liste ein. Ändert ein Thread eine Variable, benachrichtigt dieser alle Threads, die diese Variable gelesen haben. 4.3 Erweiterung der Transaktionsbibliothek um Invarianten Die in Abschnitt 4.2 vorgestellte Transaktionsbibliothek von Frank Kupke und Frank Huch enthielt noch keine Unterstützung für Invarianten. Damit der Concurrent Haskell Debugger sämtliche Funktionen der Original-Bibliothek unterstützen kann, müssen alwaysSucceeds und always noch implementiert werden. 4.3.1 Erzeugen von Invarianten Beim Ausführen von alwaysSucceeds wird die übergebene Invariante direkt über- prüft und bei erfolgreicher Ausführung in ein neues Feld (newInvar) des Transaktionszustandes eingetragen, damit sie am Ende der Transaktion erneut überprüft werden kann. Falls die Invariante Schreibaktionen oder selbst wieder Invarianten enthalten sollte, so werden diese verworfen, indem die entsprechenden Felder des 34 4.3 Erweiterung der Transaktionsbibliothek um Invarianten Transaktionszustandes auf ihre ursprünglichen Werte zurückgesetzt werden. Listing 4.21 zeigt die Implementierung. alwaysSucceeds :: STM a -> STM () alwaysSucceeds = doCheck ( stm >> return ()) where doCheck :: STM () -> STM () doCheck stm@ ( STM stmAction ) = STM (\ stmState@TST { writtenTVars = fWritten , commit = fCommit , notify = fNotify , restore = fRestore , newInvars = fInvars } -> do res <- stmAction stmState case res of Success newState _ -> do newInvID <- getGlobalInvId let newInvar = Invariant newInvID ( return ()) stm return ( Success newState { writtenTVars = fWritten , commit = fCommit , notify = fNotify , restore = fRestore , newInvars = newInvar : fInvars } ()) _ -> return res ) Listing 4.21: Implementierung von Mithilfe von always alwaysSucceeds lässt sich always alwaysSucceeds nun sehr einfach denieren. Die übergebene Invariante schlägt auch fehl, wenn sie als Ergebnis rückgibt. Es reicht also aus, alwaysSucceeds assert zu- mit einer Invariante aufzurufen, die eine Exception auslöst, falls die ursprüngliche Invariante 4.22 zeigt, wie dies durch False False zurückgibt. Listing gelingt. always :: STM Bool -> STM () always stm = alwaysSucceeds ( stm > >= (\ b -> assert b ( return ())) Listing 4.22: Implementierung von In orElse oder catchSTM always verschachtelte Transaktionen können ebenfalls neue Inva- rianten erzeugen. Damit im Fall eines Abbruchs der verschachtelten Transaktion die Invarianten nicht erhalten bleiben, muss auch das Feld newInvars wieder auf den ursprünglichen Wert zurückgesetzt werden. 4.3.2 Überprüfung von Invarianten am Ende von Transaktionen Am Ende einer Transaktion sollen nicht nur neue, sondern auch Invarianten getestet werden, die Variablen lesen, die während der Transaktion geändert wurden. Dafür 35 4 Transaktionen in Haskell erhält jede TVar zusätzlich eine Liste mit Invarianten. Während der Ausführung der Transaktion wird diese Liste zusammen mit der eindeutigen writtenTVars TVar-Nummer im Feld des Transaktionszustandes gesammelt. Nach dem Test einer Invariante können sich die von ihr gelesenen Variablen geändert haben. Daher müssen Möglichkeiten geschaen werden, eine Invariante wieder aus den alten Variablen auszutragen und in die neu gelesenen einzutragen. Dazu enthalten alle Invarianten eine Aktion, die diese Invariante aus allen Variablen austrägt. Ein weiteres Feld addInvars im Transaktionszustand sammelt Aktionen, die getestete Invarianten in die von ihnen gelesenen Variablen einträgt. Damit nach dem Ausführen einer Invariante überhaupt festgestellt werden kann, welche Variablen gelesen wurden, muss auch diese Information gesammelt werden. Dies geschieht in einem neuen Feld readTVars readTVar. IORef wieder für durch die STM-Aktion Listing 4.23 zeigt die neuen Typen. Auch hier ist die verschachtelte die Konsistenzprüfung notwendig. data TVar a = TVar ( MVar ( IORef a )) -- value ID ( MVar [ MVar ()]) -- wait queue ( MVar ( IORef [ Invariant ])) -- list of Invariants data Invariant = Invariant ID ( IO ()) ( STM ()) data StmState = TST { writtenTVars :: [( ID , MVar ( IORef [ Invariant ]))] , readTVars :: [( ID , MVar ( IORef [ Invariant ]))] , addInvars :: IO () , ...} Listing 4.23: Geänderte Typen für Invarianten Die Funktion checkInvar (Listing 4.24) testet eine Invariante am Ende einer Trans- aktion. Bei einem erfolgreichen Test werden zwei IO-Aktionen erstellt. Die Aktion removeInvarAction entfernt eine Invariante aus allen gelesenen Variablen. Die Aktion addAction trägt die Invariante inklusive der removeInvarAction in die gelesenen Variablen ein. Auch hier werden alle Schreibaktionen, die die Invariante eventuell durchgeführt hat, verworfen. Auÿerdem wird die neue im Feld addInvars addAction mit den anderen kombiniert. Nun muss nur noch die Funktion atomically so geändert werden, dass die nötigen Invarianten auch getestet werden. Zunächst wird aus den Invarianten der geschriebenen TVars STM-Aktion und den während der Transaktion erzeugten neuen Invarianten eine erzeugt, die die Invarianten überprüft und im Erfolgsfall das ursprüngli- checkInvar selbst vom STM ist. Es ist möglich, dass ein anderer Thread eine der Invariantenlisten ändert. che Ergebnis zurückliefert. Dies ist einfach, da die Funktion Typ Damit in diesem Fall die Transaktion neu gestartet wird, muss die Konsistenzprüfung erweitert werden. Die Funktion 36 isValidRef überprüft, ob eine IORef noch mit 4.3 Erweiterung der Transaktionsbibliothek um Invarianten checkInvar :: Invariant -> STM () checkInvar ( Invariant id _ stm@ ( STM stmAction )) = STM (\ stmState@TST { writtenTVars = fWritten , commit = fCommit , notify = fNotify , restore = fRestore , addInvars = fAdd } result <- stmAction stmState { readTVars = []} case result of Success newState res -> do let removeInvarAction = mapM_ (( removeInvariant id ). snd ) ( readTVars newState ) newInvar = Invariant id removeInvarAction stm addAction = mapM_ (( addInvariant newInvar ). snd ) ( readTVars newState ) return ( Success ( newState { writtenTVars = fWritten , commit = fCommit , notify = fNotify , restore = fRestore , addInvars = addAction >> fAdd }) () _ -> return result Listing 4.24: Testen einer Invariante am Ende einer Transaktion MVar gespeicherten übereinstimmt. Auf diese Weise überprüft die Aktion isValidInvarRefs, ob noch alle Listen von Invarianten unverändert sind. Jede In- der in einer variante enthält eine Aktion, die sie aus allen Transaktionsvariablen austragen kann. Diese Aktionen werden kombiniert und zusammen mit der neuen Konsistenzprüfung im Transaktionszustand übergeben, um die Invariantenprüfung zu starten. Der restliche Teil von atomically läuft mit einer Ausnahme fast wie gehabt. Bei erfolgreicher Invarianten- und Konsistenzprüfung werden die Aktion removeInvars addInvars und die während der Ausführung der Invarianten gesammelte Aktion ausgeführt, um die Invarianten nur in die korrekten TVars einzutragen. 4.3.3 Probleme bei der Invariantenprüfung In der Version der STM-Bibliothek ohne Invarianten wurden die Schreibaktionen aus Performancegründen als IO-Aktion gepuert. Zurecht wurde argumentiert, dass das aufwändige Ausführen und wieder Rückgängigmachen dieser Aktion beim Lesen einer TVar, die innerhalb derselben Transaktion bereits geschrieben wurde, recht selten notwendig sei. Durch die Einführung von Invarianten hat sich dies jedoch geändert. Jede Invariante, die durch das Ändern einer Variablen am Schluss einer Transaktion überprüft werden muss, liest auch mindestens eine Variable, die geschrieben wurde. Dies kann die Laufzeit von Transaktionen deutlich verlangsamen. 37 4 Transaktionen in Haskell atomically :: STM a -> IO a atomically stmAction = do stmResult <- startSTM stmAction stmResult ' <- case stmResult of Success newState res -> do let invarMVars = map snd ( writtenTVars newState ) invarRefs <- mapM readMVar invarMVars invarLists <- mapM readIORef invarRefs let oldInvars = nub ( concat invarLists ) invarList = newInvars newState ++ oldInvars STM invarCheckSTM = mapM_ checkInvar invarList >> return res isValidInvarRefs cont = foldl ( > >+) cont ( zipWith isValidRef invarRefs invarMVars ) removeInvarAction = mapM_ (\( Invariant _ rem _) -> rem ) oldInvars checkState = newState { addInvars = ( return ()) , isValid = isValidInvarRefs ( isValid newState ) , removeInvars = removeInvarAction } catch ( invarCheckSTM checkState ) (\ e -> return ( Exception checkState e )) _ -> return stmResult ... Listing 4.25: Testen der Invarianten in 38 atomically 4.3 Erweiterung der Transaktionsbibliothek um Invarianten Zwei verschiedene Ansätze könnten helfen, dieses Problem zu verkleinern. So wurden in einer frühen Version der STM-Bibliothek die vorläug geschriebenen Werte einer jeden Transaktion in einer zur TVar gehörenden Datenstruktur gespeichert. Diese Implementierung war zwar langsamer als die jetzt gewählte, könnte jedoch zusammen mit den Invarianten wieder besser sein. Die zweite, von Frank Kupke vorgeschlagene Möglichkeit ist, anstatt eine für jede geschriebene TVar commit-Aktion für die gesamte Transaktion zu sammeln, eine eigene Aktion vorzuhalten. Wird in diesem Fall eine geschriebene Variable wieder gelesen, so genügt es, eine vergleichsweise kleine Aktion auszuführen und wieder rückgängig zu machen. 39 4 Transaktionen in Haskell 40 5 Debugging in Haskell 5.1 Herkömmliche Debugger 5.1.1 HOOD Der Haskell Object Observation Debugger, kurz HOOD, von Andy Gill [7] ist eine Haskell-Bibliothek, die dem Benutzer erlaubt, von ihm ausgewählte Werte zu beobachten. Dem Benutzer steht dazu die Funktion Das erste Argument vom Typ String observe (Listing 5.1) zur Verfügung. ist dabei ein Label, das später bei der Zuord- observe :: ( Observable a ) = > String -> a -> a Listing 5.1: Typ von observe nung des beobachteten Wertes zum Aufruf von observe verhält sich die Funktion dient. Für das Programm observe "label" wie die Identität id. Als Seiteneekt wird observe der ihr übergebene Wert jedoch gespeichert. Dabei wirkt sich der Aufruf von nicht auf die Auswertungsreihenfolge aus. Unausgewertete Ausdrücke bleiben auch unausgewertet. Am Ende des Programmdurchlaufs werden dann die Label zusammen mit den beobachteten Werten ausgegeben. Unausgewertete Ausdrücke werden durch ein '_' dargestellt, Funktionen durch die im Programmablauf aufgetretenen Paare von Argumenten und Ergebnis. Die Einschränkung von observe auf Instanzen der Klasse Observable hat keine allzu groÿen Auswirkungen. HOOD stellt bereits Instanzen für alle Basistypen und die gebräuchlichsten Sammeltypen zur Verfügung. Auÿerdem ist es möglich, eigene Instanzen der Klasse Observable zu denieren. 5.1.2 Hat Hat, der Haskell Tracer [12], basiert auf einer Arbeit von Jan Sparud und Colin Runciman [21]. Er ermöglicht dem Anwender, Informationen über die Berechnungen in einem Haskell Programm zu erhalten. Dies geschieht in drei Schritten: 1. Zunächst wird der Quellcode eines Programms durch einen Präprozessor mit Funktionen angereichert, die eine Tracedatei erzeugen können. 41 5 Debugging in Haskell 2. Der durch den Präprozessor erzeugte Code verhält sich genauso wie das ursprüngliche Programm, generiert dabei aber zusätzlich eine Trace-Datei, die jede im Programm ausgeführte Berechnung enthält. Dabei werden Ausdrücke auch nur soweit ausgewertet, wie dies das ursprüngliche Programm getan hätte. 3. Um durch die erzeugte Trace-Datei, die unter Umständen sehr groÿ sein kann, zu navigieren, steht eine Reihe von Werkzeugen zur Verfügung. Um den Trace etwas kompakter zu halten und unwichtige Informationen zu verbergen, können Module, die als richtig angesehen werden, vom Trace ausgeschlossen werden. Dieses Prinzip wird als Trusting bezeichnet. Im Folgenden wird eine Auswahl von Werkzeugen zur Betrachtung des erzeugten Traces kurz vorgestellt. 5.1.2.1 Hat-Observe Das von Hood inspirierte Werkzeug Hat-Observe ermöglicht, die Werte von Konstanten und Funktionen im Programm zu betrachten. Funktionen werden dabei durch eine Liste aller im Programmablauf aufgetretenen Paare aus Argumenten und Ergebnis dargestellt. Da eine Funktion während eines Programmdurchlaufs unter Umständen mit einer sehr groÿen Anzahl unterschiedlicher Argumente aufgerufen wird, kann diese Liste auch ziemlich lang und unübersichtlich werden. Daher bietet Hat-Observe auch die Möglichkeit, Informationen nach verschiedenen Kriterien zu ltern. 5.1.2.2 Hat-Trail Mit Hat-Trail lässt sich verfolgen, wie ein berechnetes Ergebnis zustande gekommen ist. Begonnen wird bei der Ausgabe des Programms. Der Benutzter kann dabei einen Teil des Ergebnisses auswählen und bekommt angezeigt, welcher Funktionsaufruf zu diesem geführt hat. Nun kann sich der Benutzer weiter durchhangeln und Angaben darüber erhalten, woher ein Funktionsaufruf stammt oder wie dessen Argumente zustande gekommen sind. Da dieselben Funktionen und Konstanten oft an verschiedenen Stellen des Quellcodes verwendet werden, zeigt Hat-Trail durch Dateiname sowie Zeilen- und Spaltennummern an, an welcher Stelle im Quelltext sich der aktuelle Funktionsaufruf bendet. Auÿerdem lässt sich ein Fenster mit dem entsprechenden Quelltext önen, wobei der Cursor den Funktionsaufruf kennzeichnet. 42 5.1 Herkömmliche Debugger 5.1.2.3 Hat-Delta Mit dem Werkzeug Hat-Delta, früher Hat-Detect, lassen sich interaktiv Fehler in Programmen lokalisieren. Dem Benutzer werden dabei eine Reihe von Fragen gestellt. Jede dieser Fragen betrit einen Funktionsaufruf und das berechnete Ergebnis. Beantwortet werden muss, ob die Berechnung richtig ist. Eine fehlerhafte Funktionsdenition ist gefunden, wenn eine Funktion ein falsches Ergebnis liefert, aber jeder Funktionsaufruf im Funktionsrumpf korrekt war. Funktionen, die als Trusted gekennzeichnet sind, werden dabei nicht abgefragt. 5.1.2.4 Hat-Stack Wird ein Programm durch eine Fehlermeldung oder durch den Benutzer abgebrochen, so lässt sich mit Hat-Stack anzeigen, welche Funktion gerade berechnet wurde. Angezeigt wir dabei ein virtueller Stack von Funktionsaufrufen, so dass nachvollzogen werden kann, wie die letzte Berechnung zustande kam. 5.1.2.5 Hat-Explore Hat-Explore ermöglicht dem Benutzer, sich ähnlich wie bei einem imperativen De- bugger schrittweise durch das Programm zu bewegen. Da das Programm aber schon komplett ausgeführt wurde, ist der Benutzer nicht an die tatsächliche Auswertungsreihenfolge gebunden, sondern kann bei jedem Ausdruck anhand des Quelltextes frei wählen, an welchem Teilausdruck er interessiert ist. Ein virtueller Stack zeigt dabei, welche Funktionsaufrufe zum aktuellen Ausdruck geführt haben. Da zu jedem Ausdruck der Wert angezeigt wird, zu dem er ausgewertet wird, ist es damit möglich, zur Ursache einer fehlerhaften Auswertung zu navigieren. 5.1.2.6 Weitere Tracebetrachter Es existieren noch einige weitere Werkzeuge zur Auswertung des von Hat erzeugten Traces: Hat-Cover stellt die Codeabdeckung eines Programmdurchlaufs durch Markieren des Quelltextes dar. Hat-Anim stellt die durchgeführten Reduktionen eines Ausdrucks Schritt für Schritt dar. Pretty-Hat stellt die Trace-Datei als Graph dar. Hat-Nonterm zeigt einige Reduktionen einer Funktion, die wahrscheinlich an einer nicht terminierten Programmausführung beteiligt war. 43 5 Debugging in Haskell 5.1.3 Buddha Der Debugger Buddha (Bernie's Ultimate Declarative Debugger for Haskell) von Bernhard Pope [19, 3] stellt die Funktionsaufrufe eines Programms als Baum dar. Dem Benutzer wird dabei ermöglicht, in diesem Baum zu navigieren. Durch das Beantworten von Fragen, die die Korrektheit von Funktionsergebnissen betreen, ist es möglich, eventuell vorhandene Fehler zu lokalisieren. Der Baum, auch Evaluation Dependence Tree (EDT), stellt die Funktionsaufrufe des Programms, der Struktur des Quellcodes entsprechend, dar. Jeder Knoten enthält dabei den Namen der Funktion, die Argumente, auf die sie angewendet wurde und das berechnete Ergebnis. Argumente und Ergebnis werden soweit dargestellt, wie sie vom Programm ausgewertet wurden. Die Verwendung von Buddha hat keinen Einuss auf die Auswertung. Unausgewertete Ausdrücke werden durch ein '?', Funktionen durch die im Programm aufgetretenen Paare von Argumenten und Ergebnis dargestellt. Die möglicherweise leere Liste von Kindknoten stellt die im Funktionsrumpf aufgetretenen Funktionsaufrufe dar. Auÿerdem enthält jeder Knoten Informationen über Modul und Zeilennummer. Um den Debugger zu benutzen, muss zunächst ein Präprozessor den Quelltext so modizieren, dass während der Programmausführung zusätzlich der EDT aufgebaut wird. Wird das modizierte Programm gestartet, verhält es sich zunächst wie das ursprüngliche Programm. Sobald das ursprüngliche Progamm jedoch terminieren würde oder der Benutzer dieses abbricht, wird der Debugger gestartet. Nun hat der Benutzer die Möglichkeit, vom Wurzelknoten aus, der die Funktion main darstellt, durch die Auswahl eines Kindknotens durch den Baum zu navigieren. An jeder Stelle im Baum kann der Benutzer mit der Fehlersuche beginnen, bei Buddha wird dies als Judgement bezeichnet Dazu markiert er den aktuellen Knoten als falsch. Nun werden die Kindknoten überprüft. Ein Fehler in einer Funktionsdenition ist gefunden, falls ein als falsch markierter Knoten nur richtige Kindknoten hat. Um den Baum kleiner zu halten, können Funktionen weggelassen werden, denen vertraut wird. Rufen diese wiederum Funktionen auf, die nicht als vertrauenswürdig eingestuft sind, so werden sie als Kindknoten der nächsthöheren, nicht vertrauenswürdigen Funktion dargestellt. Zusätzlich zur interaktiv textuellen Darstellung des EDT kann Buddha diesen auch grasch darstellen. Dazu wird der Baum im dot-Format abgespeichert und kann dann durch ein externes Werkzeug als Bild dargestellt werden. Genauso lassen sich mit Buddha auch Datenstrukturen darstellen, die textuell schwer darstellbar sind. 44 5.2 Concurrent Haskell Debugger 5.2 Concurrent Haskell Debugger Die in Kapitel 5.1 vorgestellten Debugger erlauben primär das Beobachten von Werten, die während eines Programmdurchlaufs berechnet werden. Durch die bei Concurrent Haskell eingeführte Nebenläugkeit kommen jedoch weitere Fehlerquellen hinzu, dazu gehören Deadlocks, Lifelocks und fehlender gegenseitiger Ausschluss. Auÿerdem sorgt der Scheduler dafür, dass nebenläuge Programme nicht mehr deterministisch ablaufen. Fehlerhafte Programme können daher auch eine groÿe Anzahl von Tests bestehen, ohne dass sich der Fehler tatsächlich auswirkt. Tritt dieser dann doch auf, so wird die Fehlersuche dadurch erschwert, dass er sich oft nicht reproduzieren lässt. Sowohl Hat als auch Buddha können mit Concurrent Haskell nicht umgehen. Mit Hood lassen sich zwar auch Werte in nebenläugen Programmen beobachten, aller- dings lassen sich auf diese Weise die eben beschriebenen Fehler meist nicht nden. Es wird also ein Debugger benötigt, der dem Benutzer Informationen über das nebenläuge Verhalten von Threads zukommen lässt, das heiÿt, welcher Thread wann Zugri auf welche Kommunikationsabstraktionen hat und wann Threads suspendiert sind. Um nicht nur ein zufälliges Scheduling beobachten zu können, ist zusätzlich wünschenswert, dass der Benutzer Einuss auf das Scheduling erhält. Ein Debugger, der diese Anforderungen erfüllt, ist der Concurrent Haskell Debugger. Dieser wurde im Rahmen einer Diplomarbeit [1, 2] von Thomas Böttcher entwickelt und von Frank Huch betreut. 5.2.1 Starten des Debuggers Um den Concurrent Haskell Debugger benutzen zu können, reicht es, anstatt der eigentlichen Concurrent-Bibliothek das Modul CHD.Control.Concurrent zu impor- tieren. Nach dem Compilieren wird beim Ausführen des Programms automatisch auch der Debugger gestartet. Zum Umfang des CHD Pakets gehört auch ein Präprozessor, der eine transformierte Version des Quelltexts erstellt und diesen compiliert. Der transformierte Quelltext enthält neben der geänderten import-Anweisung auch Informationen über Da- teiname und Zeilennummern, die bei der Darstellung im Debugger helfen. Sowohl der geänderte Quelltext als auch das compilierte Programm benden sich dann im Unterverzeichnis Preprocess. 5.2.2 Das Hauptfenster Anstatt Informationen über ein Programm während eines Programmdurchlaufes zu sammeln und dann am Ende auf irgendeine Weise wiederzugeben, verfolgt der Concurrent Haskell Debugger einen anderen Ansatz. Dabei wird der aktuelle Zustand 45 5 Debugging in Haskell eines nebenläugen Programms während der Ausführung grasch dargestellt. Abbildung 5.1 zeigt das Beispiel der dinierenden Philosophen aus Listing 3.3. Abbildung 5.1: Screenshot des Concurrent Haskell Debuggers Auf der linken Seite werden die Threads als Kreise dargestellt. Die verschiedenen Farben sind Indikatoren für deren jeweiligen Zustand. Der Text neben jedem Thread gibt an, welche nebenläuge Aktion jener gerade ausführt. Die rechte Seite zeigt die Kommunikationsabstraktionen, in diesem Beispiel ausschlieÿlich MVars. Dazwischen zeigen Pfeile, auf welche Variable oder auch welchen anderen Thread sich die aktuelle Aktion bezieht. 5.2.2.1 Threads Die im Programm enthaltenen Threads werden als Kreise auf der linken Seite des Hauptfensters angezeigt. Direkt über dem Kreis wird ein Name dargestellt, der zur Unterscheidung der Threads dient. Durch die verschiedenen Farben lassen sich die Zustände, in denen sich die Threads gerade benden, leicht unterscheiden: Grün bedeutet, dass der Thread gerade läuft und zum Beispiel eine funktionale Berechnung ausführt oder auf eine Benutzereingabe wartet. 46 5.2 Concurrent Haskell Debugger Gelb werden Threads angezeigt, die eine nebenläuge Aktion ausführen wollen, jedoch erst die Freigabe durch den Benutzer benötigen. Diese Freigabe lässt sich durch einen Mausklick auf den Kreis des Threads erteilen. Rot zeigt an, dass der Thread durch das Ausführen der nächsten Aktion suspendieren würde. Dies kann zum Beispiel ein Lesezugri auf eine leere MVar sein. Einem Thread, der rot dargestellt wird, kann keine Freigabe für diese Aktion erteilt werden. Blau ausgefüllte Kreise stehen für Threads, die durch die Aktion threadDelay für eine bestimmte Zeit suspendiert wurden. 5.2.2.2 Kommunikationsabstraktionen Auf der rechten Seite werden die Kommunikationsabstraktionen als Rechtecke dargestellt. Der darüberstehende eindeutige Name zeigt den Typ der Kommunikationsabstraktion an. Leere Rechtecke stellen auch leere Datenstrukturen dar. Sind die Rechtecke ausgefüllt, so enthalten sie einen Wert. Ein Kürzel innerhalb des ausgefüllten Rechteckes zeigt an, welcher Thread den aktuellen Wert geschrieben hat. Obwohl die Datenstruktur Chan durch mehrere MVars implementiert ist, wird diese der abstrakten Sichtweise entsprechend dargestellt. Enthält ein Channel mehr als einen Wert, so werden diese wie in Abb. 5.2 als eine Reihe von Rechtecken hintereinander dargestellt. Die Kürzel in den einzelnen Zellen bezeichnen dabei wie bei den MVars den Thread, der den Wert geschrieben hat. Abbildung 5.2: Darstellung eines Channels im CHD Auch die anderen Datenstrukturen aus der Concurrent-Bibliothek besitzen jeweils eine eigene Darstellung. Da Kommunikationsabstraktionen Werte eines beliebigen Typs enthalten können, ist es nicht möglich diese anzuzeigen. Selbst wenn dies zum Beispiel durch die Einschränkung auf die Klasse Show ermöglicht würde, würde sich durch das Anzeigen die Auswertungsreihenfolge ändern. Um dem Benutzer dennoch die Möglichkeit zu geben, einen Hinweis darauf zu erhalten, welche Werte gerade in einer Datenstruktur stehen, kann dieser bei Schreibaktionen ein zusätzliches Label angeben. Zu jeder Aktion, die einen Wert in eine Kommunikationsabstraktion schreibt, wie zum Beispiel putMVar, existiert eine entsprechende Aktion putMVarLabel, die eine zusätzliche Zeichenkette als Argument erwartet. Diese Zeichenkette wird dann wie in Abb. 5.3 als Inhalt der Kommunikationsabstraktion angezeigt. 47 5 Debugging in Haskell Abbildung 5.3: Darstellung eines Labels als Inhalt einer Da die Aktionen mit dem Zusatz Label MVar nicht zur Cocurrent-Bibliothek gehören, müssten diese eigentlich mühsam von Hand aus dem Programm entfernt werden, bevor es sich wieder ohne den CHD compilieren lassen würde. Um dies zu vermeiden, CHD.Control.ConcurrentLess. Dieses MoControl.Concurrent, enthält aber zusätzlich die Aktionen enthält die CHD-Bibliothek das Modul dul exportiert das Modul mit dem Label-Zusatz. Diese ignorieren die übergebene Zeichenkette und rufen die entsprechende Aktion ohne Label auf. 5.2.2.3 Nebenläuge Aktionen Die Darstellung der von den Threads ausgeführten Aktionen geschieht auf zweierlei Weise. Zum einen zeigt ein Label links neben jedem Thread an, welche Aktion dieser gerade ausführt. Für die meisten Aktionen existieren zwei verschiedene Label. Das erste enthält das Wort Suspend und zeigt an, dass die Aktion als nächste ausgeführt wird. Das zweite zeigt an, dass die Aktion als letzte durchgeführt wurde. Im Fall von putMVar heiÿen die Label dann MVarPutSuspend und MVarPut. Die Label für andere Aktionen sind genauso aufgebaut. Zum anderen zeigen im Raum zwischen Threads und Kommunikationsabstraktionen Pfeile an, worauf sich die Aktionen beziehen. Ist die Aktion noch nicht durchgeführt worden, so wird der Pfeil dünn dargestellt. Nach der Aktion wird der Pfeil für kurze Zeit dick und verschwindet dann. Die verschiedenen Farben kennzeichnen verschiedene Klassen von Aktionen: Grün Rot zeigt an, dass es sich um eine Schreibaktion handelt. stellt eine Leseaktion dar. Blau zeigt an, dass eine neue Kommunikationsabstraktion erzeugt wird. Grau stellt das Erzeugen eines neuen Threads dar. Schwarz wird das Beenden eines Threads durch killThread dargestellt. 5.2.2.4 Weitere Bedienelemente Neben dem Erteilen der Freigabe für einzelne Threads kann der Benutzer auch auf andere Weise Einuss auf das Scheduling und die Darstellung nehmen. Über die Buttons der Symbolleiste kann das Scheduling aller Threads beeinusst werden: 48 5.2 Concurrent Haskell Debugger Erteilt allen wartenden Threads die Freigabe zur weiteren Ausführung. Erteilt allen Threads eine generelle Ausführungsfreigabe. Widerruft die generelle Ausführungsfreigabe für alle Threads. Über einen Dialog, der über das View -Menü zu erreichen ist, kann eingestellt werden, bei welchen Aktionen Threads auf eine Freigabe durch den Benutzer warten müssen. Auf diese Weise werden Threads nur bei für den Benutzer interessanten Aktionen angehalten. Falls ganze Threads oder Kommunikationsabstraktionen für den Benutzer uninteressant sind, so lassen sich diese verstecken. Versteckte Elemente sind im Hauptfenster nicht mehr sichtbar. Ein versteckter Thread wird im Hintergrund weiter ausgeführt und nicht angehalten. Führt ein sichtbarer Thread eine Aktion auf eine nicht sichtbare Kommunikationsabstraktion aus, so wird der Thread nicht unterbrochen. Über einen Dialog lassen sich versteckte Elemente auch wieder sichtbar machen. 5.2.3 Die Quelltextanzeige Manchmal ist es anhand des dargestellten Verhaltens recht schwer zu bestimmen, welchen Teil des Programms ein bestimmter Thread gerade ausführt. Wurde der Präprozessor benutzt, so erhält der Debugger Informationen über die Position der aktuellen Aktionen im Quelltext. Über die Menüleiste lässt sich ein weiteres Fenster önen, das den Quelltext anzeigt. Die aktuelle Aktion eines jeden Threads ist dabei hervorgehoben. Abbildung 5.4 zeigt dieses Fenster. 5.2.4 Funktionsweise 5.2.4.1 Nachrichten Die dem Concurrent Haskell Debugger zugrunde liegende Idee ist, die nebenläugen Aktionen der einzelnen Threads beobachten zu können. Um dies zu erreichen, müssen sämtliche in der Concurrent-Bibliothek vorhandenen Aktionen durch solche ersetzt werden, die zusätzlich den Debugger von der Aktion unterrichten. Ein erster Ansatz, um dies zu erreichen, könnte wie in Listing 5.2 aussehen. Die ursprüngliche Concurrent-Bibliothek wird qualiziert importiert. So ist es möglich, durch das Präx 'C.' auch die ursprünglichen Aktionen aufzurufen. Die neu denierte Aktion putMVar kann sich dadurch genauso verhalten wie erwartet, sendet aber vor und nach Ausführung der Aktion eine Nachricht an den Debugger. Auf ähnliche Weise können auch die anderen Aktionen der Concurrent-Bibliothek erweitert werden. 49 5 Debugging in Haskell Abbildung 5.4: Screenshot der Quelltextanzeige putMVar mvar val = do sendDebugMsg MVarPutSuspend C . putMVar mvar val sendDebugMsg MVarPut Listing 5.2: Erster Ansatz für putMVar 5.2.4.2 Starten des Debuggers Durch das Ersetzen des Concurrent-Moduls durch ein Modul, das die erweiterten Aktionen enthält, lassen sich zusätzliche Nachrichten erzeugen. Diese Nachrichten sollen nun jedoch auch empfangen, interpretiert und dargestellt werden. Um diese Aufgaben kümmert sich ein zusätzlicher Thread, der gestartet wird, sobald die erste Nachricht gesendet wird. Listing 5.3 zeigt, wie Nachrichten gesendet werden und dabei der Debugger gestartet wird. Wird eine Nachricht durch sendDebugMsg gesendet, so wird zunächst die ThreadId des sendenden Threads ermittelt. Diese wird zusammen mit der eigentlichen Nachricht in den Nachrichtenkanal debugMsgChan geschrieben und ermöglicht dem De- bugger zu erkennen, welcher Thread die Nachricht geschickt hat. Der Nachrichtenkanal selbst ist vom Typ DebugMsgChan. Dieser funktioniert im Prin- zip änlich wie ein gewöhnlicher Nachrichtenkanal, ermöglicht jedoch zusätzlich das 50 5.2 Concurrent Haskell Debugger debugMsgChan :: DebugMsgChan debugMsgChan = unsafePerformIO ( do dbgChan <- newDblChan myId <- C . myThreadId writeDblChanLess dbgChan ( myId , ProgramStart ) C . forkIO startChd return dbgChan ) sendDebugMsg :: DebugMsg -> IO () sendDebugMsg message = do myId <- C . myThreadId writeDblChanLess debugMsgChan ( myId , message ) Listing 5.3: Denition des Nachrichtenkanals und Starten des Debuggers Senden von Nachrichten mit höherer Priorität. Der Nachrichtenkanal debugMsgChan ist als globale Konstante deniert und wird daher nur beim ersten Aufruf ausgewertet (siehe Abschnitt 2.4.1.5). Beim Senden der ersten Nachricht wird also zunächst ein neuer Nachrichtenkanal erzeugt, der dann hält. Dann wird durch C.forkIO startChd ProgramStart als erste Nachricht ent- der Debugger-Thread gestartet, der die Nachrichten der Reihe nach aus dem Kanal liest und darstellt. Zurückgegeben wird der Nachrichtenkanal selbst, in den von nun an alle Nachrichten geschrieben werden. 5.2.4.3 Identizierung von Kommunikationsabstraktionen Bisher ist es einem Debugger, der die Nachrichten aus dem Kanal liest, möglich zu erkennen, welche Threads in welcher Reihenfolge welche nebenläugen Aktionen durchführen. Im Gegensatz zu Threads haben Kommunikationsabstraktionen kein mit der ThreadId vergleichbares eindeutiges Identikationskennzeichen. Damit der Debugger zwischen verschiedenen Kommunikationsabstraktionen unterscheiden kann, müssen deren Datenstrukturen um einen Identikator erweitert werden. Dieser wird dann als Teil der Nachricht an den Debugger gesendet. Listing 5.4 zeigt die neue Datenstruktur für MVars. Durch die enthaltene C.MVar aus der ursprünglichen data MVar a = MVar MVarNo ( C . MVar a ) Listing 5.4: Neue Datenstruktur für MVars Concurrent-Bibliothek können wie in Listing 5.2 auch die ursprünglichen Aktionen genutzt werden. Der in der neuen Denition der siert auf dem Typ Int, MVar durch den die verschiedenen enthaltene Typ MVars MVarNo ba- unterschieden werden können. Dazu muss allerdings sichergestellt sein, dass jede neue Variable eine frische MVarNo erhält. Die Aufgabe, dies zu gewährleisten, fällt dem Debugger-Thread zu. 51 5 Debugging in Haskell Ein Thread, der eine neue ger eine leere C.MVar, MVar erzeugt, sendet mit der Nachricht an den Debug- die dieser dann mit einer frischen MVarNo füllt. Der Debugger muss dabei den Überblick behalten, welche Nummern bereits vergeben sind. Wie die Zuweisung einer neuen MVar MVarNo auf der Seite des Threads funktioniert, der eine neue erzeugt, zeigt Listing 5.5. newEmptyMVar :: IO ( MVar a ) newEmptyMVar = do returnNewNoMVar <- C . newEmptyMVar sendDebugMsg ( MVarNewEmptySuspend returnNewNoMVar ) mvar <- C . newEmptyMVar mvarNo <- C . readMVar returnNewNoMVar sendDebugMsg ( MVarNewEmpty mvarNo pos ) return ( MVar mvarNo mvar ) Listing 5.5: Denition von newEmptyMVar mit Zuweisung einer neuen MVarNo 5.2.4.4 Garbage Collection Speicher, der von Datenstrukturen belegt ist, die von einem Haskell-Programm nicht benötigt und auch nicht mehr referenziert werden, wird vom Garbage Collector wieder freigegeben. Dies gilt natürlich auch für Kommunikationsabstraktionen. Damit der CHD diese nicht noch darstellt, nachdem sie schon lange aus dem Speicher entfernt worden sind, muss der Debugger-Thread benachrichtigt werden, sobald der Garbage Collector eine Kommunikationsabstraktion entfernt. Ermöglicht wird dies durch die Aktion addFinalizer des Moduls System.Mem.Weak. Diese Aktion assoziiert eine IO-Aktion mit einem Wert. Die IO-Aktion wird ausgeführt, sobald der Wert vom Garbage Collector aus dem Speicher entfernt wird. Listing 5.6 zeigt, wie wenn eine MVar addFinalizer genutzt wird, um den CHD zu benachrichtigen, aus dem Speicher entfernt wird. Diese Zeile muss vor der return- Aktion in Listing 5.5 eingefügt werden. addFinalizer mvar ( sendDebugMsg ( MVarDied mvarNo ) >> return ()) Listing 5.6: Benachrichtigung des Debuggers beim Entfernen einer MVar 5.2.4.5 Beeinussung des Scheduling Werden die in den letzten Abschnitten beispielhaft für MVars vorgestellten Techni- ken auch auf die verbleibenden Kommunikationsabstraktionen angewandt, so wird der Debugger über sämtliche nebenläuge Aktionen informiert. Bisher laufen die 52 5.2 Concurrent Haskell Debugger Threads allerdings ungehindert ab. Der Debugger bekommt dabei nur ein zufälliges Scheduling zu sehen. Durch die zusätzlichen Nachrichten kann sich dieses Scheduling durchaus relevant von dem Scheduling unterscheiden, das mit der ursprünglichen Bibliothek ausgeführt werden würde. Auÿerdem wird der Debugger möglicherweise von einer groÿen Zahl von Nachrichten überutet und kommt kaum mit der Darstellung hinterher. Der Debugger muss also die Möglichkeit erhalten, die Ausführung eines Threads zu unterbrechen. Die Unterbrechung des Threads geschieht, wann immer eine Nachricht an den Debugger-Thread gesendet wird. Dies geschieht ähnlich wie das Erfragen einer neuen Nummer für eine Kommunikationsabstraktion. Beim Senden einer Nachricht wird eine MVar erzeugt, die zusammen mit der Nachricht an den Debugger gesendet wird. MVar. Wenn der Thread fortgesetzt werden soll, wird die MVar vom Debugger gefüllt. Listing 5.7 zeigt die neuen Denitionen von sendDebugMsg und putMVar. leere Der Thread suspendiert nach dem Senden auf diese sendDebugMsg :: DebugMsg -> IO ( C . MVar ()) sendDebugMsg message = do myId <- C . myThreadId debugstop <- C . newEmptyMVar writeDblChanLess debugMsgChan ( myId , message , debugstop ) putMVar mvar val = do debugStop1 <- sendDebugMsg MVarPutSuspend C . takeMVar debugStop1 C . putMVar mvar val debugStop2 <- sendDebugMsg MVarPut C . takeMVar debugStop2 Listing 5.7: Unterbrechen von Threads durch Suspendieren 5.2.4.6 Funktionsweise des Debugger-Threads Der Debugger-Thread selbst liest aus dem debugMsgChan der Reihe nach die Nach- richten der Threads und passt den von ihm vorgehaltenen Zustand entsprechend an. In diesem Zustand nden sich die für die Darstellung notwendigen Informationen über die Threads, die aktuellen Aktionen und die Kommunikationsabstraktionen. Zusätzlich werden, bei für die Darstellung relevanten Änderungen des Zustandes, Nachrichten mit Darstellungsanweisungen an die GUI gesendet. Um Benutzereingaben umzusetzen, zum Beispiel wenn einem Thread die Freigabe zur weiteren Ausführung erteilt werden soll, schickt die GUI Nachrichten mit Priorität an den Debugger-Thread. Wie die Kommunikation zwischen den Threads, dem Debugger und der GUI stattndet, ist in Abbildung 5.5 dargestellt. 53 5 Debugging in Haskell Debugger T_1 ... Gui T_n Abbildung 5.5: Kommunikation zwischen Threads, Debugger und GUI 5.3 Concurrent Haskell Stepper Der im vorigen Abschnitt vorgestellte Concurrent Haskell Debugger ermöglicht das Debuggen von nebenläugen Haskell Programmen durch Darstellung der nebenläugen Aktionen und Beeinussung der Ausführungsreihenfolge durch den Benutzer. Um einen Fehler, zum Beispiel ein Deadlock, zu nden, muss der Benutzer durch das Auswählen einer bestimmten Ausführungsreihenfolge das Programm in einen verdächtigen Zustand bringen. Führt dies nicht zum Erfolg, so muss der Benutzer den ganzen Vorgang mit einer anderen Ausführungsreihenfolge wiederholen. Um die Suche nach Deadlocks zu erleichtern, entwickelten Jan Christiansen und Frank Huch [4] den Concurrent Haskell Stepper (CHS). Dieser testet durch iterative Deepening bis zu einer gewissen Tiefe sämtliche Ausführungsreihenfolgen. Zur einfachen Bedienung des Steppers wurde dieser in den Concurrent Haskell Debugger integriert. Während der Benutzer den Debugger wie beschrieben bedient, sucht der Stepper im Hintergrund nach Deadlocks. Ist die Suche erfolgreich, so wird der Benutzer interaktiv zum Deadlock geführt. Die zusätzlich notwendigen Änderungen am Quellcode des Programms werden von einem schon beim CHD verwendeten Präprozessor erledigt. 5.3.1 Prinzip der Deadlocksuche Die unterschiedlichen Ausführungsreihenfolgen der nebenläugen Aktionen lassen sich als Baum darstellen. Ausgehend von einem Programmzustand erreicht man den nächsten Zustand durch das Ausführen der nebenläugen Aktion eines Threads. Abbildung 5.6 zeigt einen solchen Baum. Dabei stellen die Knoten nicht notwendigerweise verschiedene Zustände eines nebenläugen Programms dar. Die Kanten stehen für die Ausführung der nächsten nebenläugen Aktion des Threads mit der angegebenen Nummer. Die Deadlocksuche in einem Programm ist dann äquivalent zur Suche im Baum. 54 5.3 Concurrent Haskell Stepper 1 1 2 3 2 1 ... 2 3 3 1 ... 2 3 ... Abbildung 5.6: Darstellung der Ausführungsreihenfolgen als Baum Die naive Herangehensweise, nach dem Testen eines jeden Pfades das Programm neu zu starten und den nächsten Pfad zu untersuchen, ist sehr aufwändig. Werden auf diese Weise zum Beispiel die beiden Pfade 1-1-1 und 1-1-2 getestet, so muss der gesamte Pfad zweimal ausgeführt werden, obwohl nur der letzte Schritt unterschiedlich ist. Bei gröÿeren Tiefen ist dies natürlich noch signikanter. Daher wurde beim CHS ein anderer Ansatz gewählt. Dieser Ansatz basiert auf der Beobachtung, dass jede nebenläuge Aktion rückgängig gemacht werden kann. Daher muss nach dem Testen des Pfades 1-1-1 nur der letzte Schritt rückgängig gemacht und ein Schritt von Thread 2 ausgeführt werden, um den Pfad 1-1-2 zu testen. 5.3.2 Redenition der IO-Monade Damit nebenläuge Aktionen schrittweise ausgeführt, wieder rückgängig gemacht und gegebenenfalls wieder ausgeführt werden können, ist es nötig, eine eigene Version der IO-Monade zu denieren. Listing 5.8 zeigt die neue Datenstruktur, die im Folgenden genauer erläutert wird. Da der Datentyp IO normalerweise automatisch im- portiert wird, wird er zur Redenition explizit versteckt. Damit der Zugri dennoch möglich ist, wird das Modul liche Datentyp IO Prelude qualiziert importiert. So können der ursprüng- und die IO-Aktionen durch das Präx P. angesprochen werden. Genauso werden nebenläuge Aktionen aus dem Modul C. angesprochen. Control.Concurrent durch 5.3.2.1 Bind und Return In Haskells IO-Monade lassen sich zwei IO-Aktionen durch (>>=) zu einer Aktion kombinieren. Hat man nur Zugri auf die kombinierte Aktion, dann kommt man an die einzelnen Aktionen nicht mehr heran. Da der CHS einzelne Aktionen ausführen 55 5 Debugging in Haskell data IO a = | | | | | forall b . Bind ( IO b ) ( b -> IO a ) Return a forall b . ConcAction (P . IO ( Maybe (a , b ))) ( b -> P . IO ()) SeqAction ( ActionType a ) ( P . IO a ) ForkAction ( IO ()) ( P . IO C . ThreadId ) KillAction C . ThreadId instance Monad IO where ( > >=) = Bind return = Return Listing 5.8: Redenierter Datentyp IO a soll, ist dies jedoch notwendig. Daher wird in der neuen IO-Monade der Kombinator b durch den Existentzquantor forall innerhalb von Bind versteckt, da dieser den Typ IO a besit- (>>=) durch den Konstruktor Bind implementiert. Dabei wird der Typ zen soll. Eine zusammengesetzte IO-Aktion wird also als eine Art Baum dargestellt. Die Blätter bestehen aus elementaren IO-Aktionen, die inneren Knoten aus Das Monadische return wird einfach durch den Konstruktor Return Binds. implementiert, der schlicht einen funktionalen Wert enthält. 5.3.2.2 ConcAction ConcActions sind die nebenläugen Aktionen aus dem Modul Control.Concurrent, also genau die Aktionen, deren Ausführung für den CHS interessant sind. Das erste Argument ist eine IO-Aktion, die die nebenläuge Aktion ausführt. Der Nothing zurückgegeben, wenn die Aktion suspendieren würde, ansonsten ist das Ergebnis Just (a,b), wobei a das Ergebnis der nebenläugen Aktion ist und b ein Wert, der benötigt wird, um die Stepper soll jedoch nicht suspendieren. Daher wird Aktion wieder rückgängig zu machen. Das zweite Argument ist eine IO-Aktion, die den Zustand vor Ausführung der Aktion wieder herstellt. Dazu wird häug ein Wert benötigt, der bei der Ausführung der nebenläugen Aktion ermittelt wurde, dieser wird als Argument übergeben. Damit die nebenläugen Aktionen in den neuen Datentyp passen, müssen auch diese neu deniert werden. Die folgenden zwei Beispiele machen deutlich, wie dies funktioniert. MVar (Listing 5.9) kann ein Thread nicht suspenJust (mvar,()) zurückgeliefert. Die Aktion muss auch werden, da der Garbage Collector die erzeugte MVar wie- Beim Erzeugen einer neuen, leeren dieren, deshalb wird immer nicht rückgängig gemacht der entfernt. Aus diesem Grund ist es irrelevant, welchen zweiten Wert die Aktion zurückliefert, hier also (), und die Wiederherstellungsaktion tut einfach nichts. Anders sieht dies bei der Aktion 56 takeMVar aus, die in Listing 5.10 dargestellt ist. 5.3 Concurrent Haskell Stepper newEmptyMVar :: IO ( MVar a ) newEmptyMVar = ConcAction ( do mvar <- C . newMVar return ( Just ( mvar ,()))) (\ _ -> return ()) newEmptyMVar Listing 5.9: Neudenition von für den CHS takeMVar :: MVar a -> IO a takeMVar mvar = ConcAction ( do b <- isEmptyMVar mvar if b then return Nothing else do v <- C. takeMVar mvar return ( Just (v , v )) (\ v -> C . putMVar mvar v ) Listing 5.10: Neudenition von takeMVar für den CHS MVar zu lesen, würde suspendieren. DesMVar leer ist. Ist dies der Fall, wird Nothing MVar geleert und der erhaltene Wert wird zu- Ein Thread, der versucht, eine bereits leere halb wird zunächst überprüft, ob die zurückgegeben. Ansonsten wird die rückgeliefert. Um diese Aktion wieder rückgängig zu machen, muss nur genau dieser Wert wieder zurückgeschrieben werden. 5.3.2.3 ForkAction und KillAction ConcActions. KillAction dargestellt Zwei Aktionen aus der Concurrent-Bibliothek gehören nicht zu den Dies sind forkIO und killThread, die als ForkAction bzw. werden (Listing 5.11). forkIO :: IO () -> IO C . ThreadId forkIO io = ForkAction io ( do threadId <- C . forkIO ( return ()) return ( threadId , threadId )) killThread :: C . ThreadId -> IO () killThread threadId = KillAction threadId Listing 5.11: Neudenition von Dabei enthält eine ForkAction forkIO und killThread für den CHS den IO-Baum des neu zu erzeugenden Threads und eine Aktion, die einen leeren Thread erzeugt, um eine eindeutige ThreadId zu er- 57 5 Debugging in Haskell halten. Dabei muss weder ein echter Thread erzeugt werden noch muss die Aktion rückgängig gemacht werden, da Threads bei der Deadlocksuche als Liste von IOBäumen repräsentiert werden. Wie dies genau geschieht, wird in Abschnitt 5.3.3 erläutert. Aus demselben Grund enthält auch die KillAction nur die threadId des zu beendenden Threads. 5.3.2.4 SeqAction Sämtliche IO-Aktionen, die nicht in der Concurrent-Bibliothek deniert sind und damit keinen oder zumindest keinen direkten Einuss auf das nebenläuge Verhalten SeqActions im IO-Baum dargestellt. Dazu gehören SeqAction hat ein Argument vom Typ ActionType (Listing 5.12) und die eigentliche IO-Aktion. Über den ActionType des Programms haben, werden als zum Beispiel Ein- und Ausgabeoperationen. Eine wird unterschieden, ob und wie die IO-Aktion während der Suche ausgeführt werden kann. data ActionType a = NonBackTrackable | Ignorable ( P . IO a ) | Executable Listing 5.12: Datentyp Bei Aktionen, die den ActionType ActionType NonBackTrackable haben, wird die Suche ab- gebrochen. Dies können zum Beispiel Eingabeoperationen sein. Von Eingaben kann der weitere Programmablauf abhängen. Da es nicht sinnvoll ist während der Deadlocksuche Eingaben zu tätigen, kann die Suche in solchen Fällen nicht fortgesetzt werden. Ignorable bedeutet, dass die Aktion ignoriert und die Suche fortgesetzt werden kann. Dies ist zum Beispiel bei Ausgabeoperationen der Fall, da Ausgaben während der Suche nicht erzeugt werden sollen. Werte vom Typ Ignorable enthalten eine IO- Aktion, die anstelle der eigentlichen Aktion ausgeführt wird. In den meisten Fällen ist dies einfach return (). Manche IO-Aktionen können auch während der Suche ausgeführt werden. Diese haben den ActionType Executable. SeqAction deniert werden. getChar. Nun müssen sämtliche elementaren IO-Aktionen neu als Listing 5.13 zeigt dies beispielhaft für putChar und 5.3.3 Suche nach Deadlocks Im vorigen Abschnitt wurde gezeigt, wie IO-Aktionen als Baumstruktur dargestellt werden und bei der Ausführung von nebenläugen Aktionen ein Suspendieren verhindert wird. Durch diese Konstruktion kann ein einziger Thread das Verhalten aller 58 5.3 Concurrent Haskell Stepper putChar :: Char -> IO () putChar char = SeqAction ( Ignorable ( return ())) ( P . putChar char ) getChar :: IO Char getChar = SeqAction NonBackTrackable P . getChar Listing 5.13: Denition von putChar und getChar für den CHS Threads simulieren. Für die Suche wird ein Thread als Paar aus einer ThreadId und dem IO-Baum seiner IO-Aktionen dargestellt (Listing 5.14). tpye Thread = ( C. ThreadId , IO ()) Listing 5.14: Datentyp Thread für die Deadlocksuche Wie bereits mehrfach erwähnt, werden Threads für die Deadlocksuche schrittweise ausgeführt. Dazu wird zunächst eine Funktion benötigt, die einen Thread einen Schritt weit ausführt. Listing 5.15 zeigt den Typ dieser Funktion. checkThread :: Thread -> P . IO Result data Result = | | | | | | Suspended Stepped ( Thread , P . IO ()) NotStepped ( Thread , P . IO ()) Stop Terminate Fork Thread ( Thread , P . IO ()) Kill C . ThreadId ( Thread , P. IO ()) Listing 5.15: Typ der Funktion Die Funktion checkThread checkThread führt die nächte Aktion im IO-Baum des Threads aus. Result gibt dabei an, wie sich der Thread verhalten würde. Das Ergebnis vom Typ Wird als Ergebnis Suspended zurückgegeben, so bedeutet dies, dass ein realer Thread beim Ausführen seiner nächsten Aktion suspendieren würde. Wird Stepped zurückgegeben, so wurde eine nebenläuge Aktion ausgeführt. Mit zurückgegeben wird der Thread mit dem verbleibenden IO-Baum und die IO-Aktion, die die soeben ausgeführte Aktion wieder rückgängig macht. NotStepped Executable ist das Ergebnis von SeqActions, die den ActionType Ignorable oder besitzen. Da hierbei keine nebenläugen Aktionen ausgeführt werden, muss auch nicht jede mögliche Ausführungsreihenfolge getestet werden. 59 5 Debugging in Haskell Das Ergebnis Stop wird zurückgegeben, wenn eine SeqAction die NonBacktrackable ist als nächste hätte ausgeführt werden sollen. Die Deadlocksuche kann an dieser Stelle nicht fortgesetzt werden. Wenn ein Thread terminieren würde, dann wird Fork und Kill sind die Ergebnisse von Terminate ForkAction Fork den weiter auszuführenden Thread zurück, bei bei Kill die ThreadId bzw. zurückgegeben. KillAction. Beide geben den neu erzeugten Thread und des zu beendenden Threads. Die eigentliche Deadlocksuche wird nun von der Funktion checkThreads durchge- führt. Sie erhält eine Liste von Threads und führt diese bis zu einer bestimmten Tiefe in jeder möglichen Reihenfolge aus. Wird dabei ein Deadlock gefunden, so wird eine Liste von ThreadIds zurückgegeben, die die Ausführungsreihenfolge der Threads, die zum Deadlock geführt hat, repräsentiert. Listing 5.16 zeigt den Typ von checkThreads. checkThreads :: [ Thread ] -> Int -> Bool -> [ Int ] -> P . IO ( Maybe [C . ThreadId ]) Listing 5.16: Typ von checkThreads Dabei ist das erste Argument die Liste der Threads, die überprüft werden. Das zweite Argument gibt an, bis zu welcher Tiefe die Suche noch hinabsteigen soll. Das Argument vom Typ Bool gibt an, ob bei den bisher getesteten Threads kein Fortschritt mehr möglich ist. Das letzte Argument gibt an, welche Threads der aktuellen Tiefe noch getestet werden müssen. Die Elemente der Liste sind Positionsangaben, die sich auf die Liste der Threads beziehen. Ein Deadlock ist gefunden, wenn alle Threads getestet wurden und kein Thread weiter ausgeführt werden kann. Listing 5.17 zeigt, wie sich die Funktion checkThreads verhält, wenn alle Threads einer Ebene getestet wurden. Dies ist der Fall, wenn die Liste der noch zu testenden Threads leer ist. Das Argument dead vom Typ Bool gibt dann an, ob ein Deadlock gefunden ist. checkThreads ( _ : _ ) _ dead [] = if dead then return ( Just []) else return Nothing Listing 5.17: Die Funktion checkThreads nach dem Testen aller Threads einer Ebene Ist ein Deadlock gefunden, so wird nach oben durchgereicht und mit Deadlock geführt hat. 60 Just [] zurückgegeben. Diese Liste wird dann den ThreadIds gefüllt, deren Ausführung zum 5.3 Concurrent Haskell Stepper Sind noch nicht alle Threads einer Ebene getestet, so wird der nächste Thread mit checkThread überprüft (Listing 5.18). checkThreads ts ( n +1) dead ( m : ms ) = do let thread = ts !!( m -1) threadId = fst thread result <- checkThread thread case result of Suspended -> checkThreads ts ( n +1) dead ms Stepped (t ' , restoreAction ) -> do let ts ' = replaceWithPos m t ' ts checkRes <- checkThreads ts ' n True [1.. lenght ts '] restoreAction case checkRes of Nothing -> checkThreads ts n1 False ms Just path -> return ( Just ( threadId : path ) ... Listing 5.18: Testen eines Threads mit checkThreads Je nachdem, welches Ergebnis diese Funktion liefert, wird weiter verfahren. Hier wird dies nur für Suspended und Stepped erläutert, für mehr Details siehe [4]. Suspended Würde ein Thread suspendieren, so werden die restlichen Threads überprüft. Ein Fortschritt ist genau dann nicht möglich, wenn auch bisher kein Fortschritt möglich war, daher wird der Parameter dead direkt übernommen. Stepped Konnte der Thread eine nebenläuge Akion durchführen, so wird die Ak- checkThreads zunächst auf die geänderte Threadliste angewandt. Der dead wird dabei zunächst auf True gesetzt, da auf der neuen Ebene noch keine Threads getestet wurden. Nach dem Aufruf von checkThreads wird tion Parameter die ausgeführte Aktion wieder rückgängig gemacht. Wurde ein Deadlock gefunden, so wird die threadId des getesteten Threads an die zurückgelieferte Liste angehängt und die neue Liste zurückgegeben. Wurde kein Deadlock gefunden, so werden die restlichen Threads auf der Ebene getestet. Der Parameter wird auf False dead gesetzt, da für den gerade getesteten Thread ein Fortschritt möglich ist. 5.3.4 Reduzierung des Suchraums Bei der im vorigen Abschnitt vorgestellten Version der Deadlocksuche wurden die nebenläugen Aktionen in jeder möglichen Reihenfolge getestet. Betreen zwei nebenläuge Aktionen jedoch unterschiedlich Kommunikationsabstraktionen, so is es unerheblich, in welcher Reihenfolge diese ausgeführt werden. Diese Eigenschaft wird genutzt, um den Suchraum der Deadlocksuche, durch ein Partial Order Reduction genanntes Verfahren zu reduzieren. 61 5 Debugging in Haskell Dazu wird die Suche zunächst so geändert, dass keine unterschiedlichen Reihenfolgen der gleichen Threads mehr getestet werden. Wird zum Beispiel die Reihenfolge 2-1-1 getestet, dann wird nicht zusätzlich 1-2-1 überprüft, wohl aber 2-2-1. Erreicht wird dies, indem nach dem Testen eines bestimmten Threads in der nächsten Ebene nicht wieder alle Threads getestet werden, sondern nur die, die in der Threadliste vor dem aktuellen Thread stehen und dieser selbst. Da natürlich nicht immer alle Threads voneinander unabhängig sind, müssen Threads, die voneinander abhängen, in jeder möglichen Reihenfolge getestet werden. Zwei Threads hängen dann voneinander ab, wenn sie eine Aktion auf dieselbe Konnunikationsabstraktion oder denselben Thread ausführen. Um dies zu identizieren, erhält der Konstruktor ConcAction ein zusätzliches Argument vom Typ Object, das die Kommunikationsabstraktion oder den Thread, auf die oder den die nebenläuge Aktion ausgeführt wird, eindeutig identiziert. Dadurch ist es möglich, eine Funktion dependentOn zu denieren, die aus einer Liste von Threads diejenigen zurückliefert, die von einer bestimmten Kommunikationsabstraktion oder einem Thread abhängen. Listing 5.19 zeigt, wie checkThreads mit Suchraumreduktion deniert ist. checkThreads ts ( n +1) dead ( m : ms ) = do let thread = ts !!( m -1) threadId = fst thread nextObj = nextObject ( snd thread ) dependList = dependentOn nextObj list = filter ( >m ) dependList result <- checkThread thread case result of Suspended -> checkThreads ts ( n +1) dead ms Stepped (t ' , restoreAction ) -> do let ts ' = replaceWithPos m t ' ts checkRes <- checkThreads ts ' n True ([1.. m ] ++ list ) restoreAction case checkRes of Nothing -> checkThreads ts n1 False ms Just path -> return ( Just ( threadId : path ) ... Listing 5.19: Im Fall von checkThreads mit Reduktion des Suchraums Suspended muss nichts geändert werden, da keine Threads in der nächsStepped aus. Zusätzlich zu den ten Ebene überprüft werden. Anders sieht dies bei Threads mit kleinerer oder gleicher Position in der Threadliste werden in der nächsten Ebene auch diejenigen überprüft, die von der durch den aktuell überprüften Thread geänderten Kommunikationsabstaktion abhängen. Wenn nun alle auf einer Ebene überprüften Threads suspendieren, heiÿt dies nicht mehr, dass ein Deadlock gefunden ist. Daher müssen in diesem Fall auch alle bisher nicht überprüften Threads darauf getestet werden, ob sie in ihrem nächsten Schritt suspendieren würden. Dies macht die Funktion 62 checkSuspended. 5.3 Concurrent Haskell Stepper 5.3.5 Integration des CHS in den CHD Die bisher vorgestellte Implementierung des CHS ist in der Lage, Deadlocks zu nden und die Ausführungsreihenfolge, die zu diesem Deadlock führt, zurückzugeben. Für einen Benutzer wäre es jedoch sehr schwierig, anhand der Ausführungsreihenfolge den Fehler im Programm zu nden. Auch Programme, die Benutzereingaben erfordern oder bei denen ein mögliches Deadlock erst nach einer groÿen Anzahl von nebenläugen Aktionen auftreten kann, sind bisher schwer oder überhaupt nicht zu durchsuchen. Aus diesen Gründen wurde der CHS in den CHD integriert. Die nötigen Änderungen am Quelltext führt wie schon beim CHD ein Präprozessor durch, der nun jedoch einen zusätzlichen Parameter erhält, über den bestimmt werden kann, ob der CHD allein oder zusammen mit dem CHS genutzt werden soll. Nach dem Starten des vom Präprozessor veränderten Programms verwendet der Benutzer den CHD wie zuvor. Immer dann, wenn der Debugger alle Threads angehalten hat, wird im Hintergrund vom aktuell angezeigten Zustand ausgehend nach einem Deadlock gesucht. Wenn der Benutzer einem Thread die Freigabe für den nächsten Schritt erteilt, wird die Suche gestoppt und von dem neuen Zustand aus wieder gestartet. Hat der CHS ein Deadlock gefunden, wird der Thread markiert, der als nächstes ausgeführt werden muss, um in den Deadlockzustand zu gelangen. Der Benutzer wird auf diese Weise sukzessive zum Deadlock geführt, kann sich aber auch dafür entscheiden, einen anderen Thread als nächstes auszuführen. Soll der CHS genutzt werden, so wird, wie in Abschnitt 5.3.2 gezeigt, der Datentyp IO neu deniert, so dass ein IO-Baum entsteht. Um die IO-Aktionen tatsächlich auszuführen, wie dies für den CHD nötig ist, wird ein Interpreter benötigt, der einen IO-Baum als Argument bekommt und ihn in der ursprünglichen IO-Monade ausführt. stepThread deniert, die die nächste IO-Aktion SeqActions (Listing 5.20) ist dies kein Problem, da diese Dazu wird zunächst eine Funktion eines Threads ausführt. Bei die ursprüngliche IO-Aktion bereits enthalten. Da die Aktion hier unwiderruich ausgeführt wird, spielt es keine Rolle, welchen ActionType diese besitzt. stepThread :: Thread -> Result stepThread ( tId , SeqAction _ sA ) = do v <- sA return ( NotStepped ( tId , Return v )) Listing 5.20: Funktion Bei stepThread für SeqActions ConcAction, ForkAction und KillAction existiert bisher noch keine so ausführ- bare Aktion. Daher benötigen diese Konstruktoren einen weiteren Parameter, der die CHD-Version der jeweiligen nebenläugen Aktion enthält. Da der CHD eine eigene Denition der Kommunikationsabstraktionen verwendet, ist auch eine neue Denition der Kommunikationsabstraktioen für den CHS nötig. Listing 5.21 zeigt, wie die neue Denition des Datentyps MVar aussieht. CD.MVar wird dabei zur Ausführung 63 5 Debugging in Haskell im CHD benötigt, die andere für die Suche. data MVar a = MVar ( CD . MVar a ) ( C . MVar a ) Listing 5.21: Datentyp MVar a im CHS Nun steht auf der einen Seite die Deadlocksuche, auf der anderen Seite die Ausführung der einzelnen Threads und die Anzeige im Debugger. Um die Kommunikation zwischen diesen beiden Seiten zu ermöglichen, wurden drei globale 1. MVars eingeführt: breakMVar wird von einem Thread gefüllt, um dem CHS anzuzeigen, dass eine nebenläuge Aktion ausgeführt werden soll. Der CHS unterbricht dann die Suche. 2. finishedBreakMVar wird vom CHS gefüllt, sobald die Suche erfolgreich unterbrochen ist und der Ausgangszustand wieder hergestellt wurde. Der Thread, der eine nebenläuge Aktion ausführen will, suspendiert auf diese MVar, um die Unterbrechung der Suche abzuwarten. 3. treeMVar wird genutzt, um nach der Ausführung einer nebenläugen Aktion den neuen IO-Baum eines Threads an den CHS zu übergeben. Daraus ergibt sich dann die am Beispiel von Denition für eine ConcAction. putMVar in Listing 5.22 dargestellte putMVar :: MVar a -> a -> IO () putMVar ( MVar cdMVar@ ( CD . MVar mvarNo _ _ _ ) searchMVar ) x = ConcAction ( MVarObj mvarNo ) (\ treeFct -> do CD . putMVarCHS ( do C . putMVar breakMVar () C . takeMVar finishedBreakMVar C . putMVar searchMVar x C . putMVar treeMVar ( treeFct ())) cdMVar x ) ( do b <- C . isEmptyMVar searchMVar if b then do C . putMVar searchMVar x return ( Just (() ,())) else return Nothing ) (\ _ -> do C . takeMVar searchMVar return ()) Listing 5.22: Denition von Von stepThread für den CHS ConcAction CD.putMVarCHS des CHD ausgeführt, die wird dabei das zweite Argument des Konstruktors ausgeführt. Dadurch wird die neue Funktion 64 putMVar 5.3 Concurrent Haskell Stepper sich von der ursprünglichen Funktion CD.putMVar nur dadurch unterscheidet, dass MVar tatsächlich geändert CD.MVar wird dies geschützt. Die übergebene sie eine übergebene IO-Aktion dann ausführt, wenn die wird. Durch ein zusätzliches Lock in der Aktion kommuniziert dabei mit dem CHS und verändert auch die vom CHS benötigte searchMVar. Durch diese nicht ganz einfache Konstruktion wird die Konsistenz der MVars des CHD und des CHS gewahrt. Da der resultierende IO-Baum, der auch in dieser Aktion kommuniziert wird, vom Resultat der CHD-Aktion abhängen kann, wird die Funktion treeFct übergeben, die den neuen IO-Baum liefert. 5.3.6 Exceptionhandling im CHS Wird in Haskell eine Exception geworfen, die nicht abgefangen wird, so wird das Programm mit der Meldung dieser Exception abgebrochen. Wird die Exception abgefangen, zum Beispiel wenn sie innerhalb eines catch-Blocks geworfen wurde, so wird, wie von Simon Peyton Jones et al. in [18] beschrieben, der Laufzeitstack so manipuliert, dass der innerste catch-Block die Exception als Wert übergeben be- kommt. Natürlich können auch während der Deadlocksuche Exceptions geworfen werden. Bisher wurde dadurch sowohl die Suche als auch der Debugger beendet. Auch war die catch-Anweisung nicht implementiert, so dass das Abfangen von Exceptions nicht möglich war. Nicht abgefangene Exceptions stellen in den meisten Fällen ungewollte Fehler dar. Sie sind insoweit mit Deadlocks vergleichbar. Daher macht es Sinn, die Deadlocksuche des CHS zu erweitern und zusätzlich auch nach nicht abgefangenen Exceptions zu suchen. Selbstverständlich muss dazu in einem Programm auch das Abfangen von Exceptions ermöglicht werden. Wie das Abfangen von Exceptions funktioniert und wie nach nicht abgefangenen Exceptions gesucht wird, wird im Folgenden erläutert. 5.3.6.1 Exceptions als Resultat Um mit Exceptions umgehen zu können, muss zunächst verhindert werden, dass das Auftreten einer Exception bei der Ausführung einer Aktion des IO-Baums nach oben durchgereicht und der Debugger beendet wird. Dazu wird die Ausführung einer jeden IO-Aktion in checkThread und stepThread durch eine catch-Anweisung gesi- chert. Tritt eine Exception auf, so wird die Exception durch einen neuen Konstruktor CHSException des Typs Result SeqAction funktioniert. weitergegeben. Listing 5.23 zeigt, wie dies bei einer Doch das Abfangen von Exceptions bei der Ausführung von IO-Aktionen reicht nicht aus, wie das Beispiel in Listing 5.24 zeigt. Hier tritt die Exception auf, sobald der IO-Baum ausgewertet wird, also zum Beispiel durch die Aktion checkThread. Dies allein wäre noch kein groÿes Problem, 65 5 Debugging in Haskell checkThread ( id ,( SeqAction Executable sA )) = catch ( do v <- sA return ( NotStepped (( id , Return v ) , return ()))) (\ e -> return CHSException e ) Listing 5.23: Beispiel für das Abfangen einer Exception bei einer IO-Aktion main = if ( 'a ' == ( assert ( 'a ' == 'b ') 'b ')) then putStrLn " foo " else return " bar " Listing 5.24: Beispiel für eine Exception bei der Auswertung des IO-Baums doch werden die IO-Bäume der Threads auch durch die Funktion dependentOn, die für die Reduzierung des Suchraums notwendig ist, ausgewertet. Da diese Funktion nicht in der IO-Monade ausgeführt wird, ist es erst möglich eine Exception abzufangen, sobald das Ergebnis der Funktion ausgewertet wird. Und an dieser Stelle ist es nicht mehr möglich zu unterscheiden, welcher IO-Baum zu der Exception geführt hat. Anstatt nun alle Funktionen, die den IO-Baum von Threads auswerten, in der IO-Monade auszuführen, wird hier ausgenutzt, dass all diese Funktionen den Baum immer nur bis zu seinem linkesten Blatt auswerten. Daher ist es möglich, eine IOAktion evalThread zu denieren, die den IO-Baum eines jeden Threads kontrolliert auswertet, bevor dies eine andere Aktion tut. Wird dabei eine Exception geworfen, so wird der Teil des Baumes, der dabei ausgewertet werden sollte durch den Konstruktor ExceptionAction ersetzt. Dieses Ersetzen scheint vom Konzept her ähnlich abzulaufen, wie dies bei Haskell auf der Laufzeitstack-Ebene gemacht wird (siehe [18]). Listing 5.25 zeigt, wie dies geschieht. evalThread :: ( C . ThreadId , IO a ) -> P . IO ( C . ThreadId , IO a) evalThread thread@ ( tId , _ ) = CE . catch ( evalThread ' thread ) (\ e -> return ( tId , ExceptionAction e )) evalThread ' ( tId , Bind io fa ) = do (_ , res ) <- evalThread ( tId , io ) return ( tId , Bind res fa ) evalThread ' thread = return thread Listing 5.25: Denition von 66 evalThread 5.3 Concurrent Haskell Stepper 5.3.6.2 Abfangen von Exceptions checkThread zurückgegeben werden, ist es recht catch zu implementieren. Dazu wird zunächst ein neuer Konstruktor Catch für den IO-Baum eingeführt. Die Funktion catch erzeugt nun lediglich einen solchen Nun, da Exceptions als Resultat von einfach, Knoten (siehe Listing 5.26). data IO a = ... | Catch ( IO a ) ( CE . Exception -> IO a ) catch :: IO a -> ( CE . Exception -> IO a) -> IO a catch io fio = Catch io fio Listing 5.26: Denition von catch und Catch checkThread mit dem neuen Knoten im Baum macht. Ist der nächste Knoten ein Catch, so wird einfach der nächste Schritt in dessen Rumpf überprüft (Listing 5.27). Wenn das Ergebnis wie bei Stepped den neuen IO-Baum enthält, wird das Catch wieder hinzugefügt. Zeigt das Ergebnis an, dass Damit bleibt zu klären, was eine Exception aufgetreten ist, so wird als neuer IO-Baum der Exceptionhandler zurückgegeben. Das Ergebnis ist dann NotStepped, da keine nebenläuge Aktion durchgeführt wurde. In allen anderen Fällen, also wenn ein Thread suspendieren würde, eine Aktion nicht ausgeführt werden kann oder der IO-Baum leer ist, wird das Ergebnis direkt zurückgegeben, da der Bereich des catch-Blocks verlassen wird. checkThread ( tId , ( Catch io handler )) = do res <- checkThread ( tId , io ) case res of Stepped (( id , newIo ) , restore ) -> return ( Stepped (( id ,( Catch newIo handler )) , restore )) ... CHSException e -> return ( NotStepped (( tId , handler e ), return ())) _ -> return res Listing 5.27: Denition von checkThread zur Behandlung von Catch 5.3.6.3 Suche nach nicht abgefangenen Exceptions Nicht abgefangene Exceptions werden dadurch identiziert, dass das Testen eines checkThread das Ergebnis CHSException liefert. In diesem ThreadId des gerade geprüften Threads direkt nach oben weitergegeben. Threads durch die Aktion Fall wird die Auf diese Weise wird genau wie bei einem Deadlock die Ausführungsreihenfolge, die zu der Exception geführt hat, gesammelt. Im CHD wird darauf der Benutzer zu dieser Exception geführt. 67 5 Debugging in Haskell 5.3.7 Unterstützung von unsafePerformIO im CHS Die Ausführung von IO-Aktionen auÿerhalb der main-Aktion durch unsafePerformIO wird häug genutzt, um globale Konstanten zu erzeugen. Soll ein Programm, das eine solche globale Konstante verwendet, mit dem CHS getestet werden, so wird sowohl bei der Suche nach einem Deadlock als auch beim Verwenden des Debuggers auf diese zugegrien. Da eine solche Konstante nur ein einziges Mal ausgewertet wird, muss diese in beiden Fällen für den Debugger sichtbar erzeugt werden. Selbstverständlich dürfen in diesem Fall die ausgeführten Aktionen auch nicht wieder rückgängig gemacht werden. Um dies zu erreichen, muss nun der IO-Baum in eine IO-Aktion verwandelt werden, die dann ausgeführt wird. Listing 5.28 zeigt am Beispiel der ConcAction, wie dies geschieht. Ausgeführt wird dabei die Aktion, die auch Nachrichten an den Debugger sendet. Wird die Aktion während der Deadlocksuche ausgeführt, so wird auch diese bei der Ausführung unterbrochen. Der Debugger erkennt jedoch, dass die Nachrichten nicht von einem der zu debuggenden Threads stammen und erteilt automatisch die Freigabe. Die erzeugten Kommunikationsabstraktionen werden daher auch bei der Erzeugung während der Deadlocksuche korrekt dargestellt. Die so ausgeführten Aktionen haben nichts mit dem IO-Baum des Threads zu tun, also muss nach deren Ausführung die Deadlocksuche auch nicht angehalten und mit einem neuen IO-Baum wieder gestartet werden. Daher bekommt die CHD-Aktion ein weiteres Argument vom Typ Bool, das angibt, ob die Aktion innerhalb von unsafePerformIO ausgeführt wird. Ist dies der Fall, ndet die in Listing 5.22 dargestellte Kommunikation mit dem CHS nicht statt. Daher kann auch die Funktion, die den neuen IO-Baum erzeugen würde, undeniert bleiben. executeUnsafely :: IO a -> P . IO a executeUnsafely ( ConcAction _ dA _ _ ) = dA True undefined unsafePerformIO :: IO a -> a unsafePerformIO io = SIU . unsafePerformIO ( executeUnsafely io ) Listing 5.28: Denition von executeUnsafely Wird also ein Ausdruck, der mit und unsafePerformIO für den CHS unsafePerformIO deniert ist, ausgewertet, so wird die enthaltene IO-Aktion direkt ausgeführt. Im Fall von globalen Konstanten ist dies auch das gewünschte Verhalten. Werden solche Ausdrücke mit Seiteneekten jedoch innerhalb von Funktionen verwendet, so kann es durch die Deadlocksuche zu unvorhersehbaren Ergebnissen kommen. Listing 5.29 zeigt ein solches Programm. Wird dieses Programm so ausgeführt, so wird es wie gewünscht terminieren. Die Aktion takeMVar wird erst ausgeführt, nachdem putMVar bereits ausgeführt wurde. Soll dieses Programm jedoch durch den CHS auf Deadlocks überprüft werden, so 68 5.3 Concurrent Haskell Stepper main = do var <- newEmptyMVar putMVar var () case unsafePerformIO ( takeMVar var ) of () -> return () Listing 5.29: Problematischer Einsatz von unsafePerformIO wird dies fehlschlagen. Zunächst wird der CHS das Programm ausführen, also einen Wert in die MVar schreiben und danach wieder herausnehmen. Ist dies geschehen, so soll der Ursprungszustand wieder hergestellt werden. Da jedoch unsafePerformIO ausgeführt wurde, wird nur führt dazu, dass aus einer leeren putMVar takeMVar durch rückgängig gemacht. Dies MVar gelesen wird und die Deadlocksuche blockiert. In realen Anwendungen sollte dieses Problem jedoch kaum auftreten, da schon in der Dokumentation von unsafePerformIO von einer derartigen Ausführung von IO- Aktionen mit Seiteneekten abgeraten wird. In einigen Programmen, so auch im CHD, wird unsafePerformIO genutzt, um neue Threads zu erzeugen. Das Debuggen solcher Programme ist mit dem CHS bisher noch nicht möglich. Der Versuch, einen neuen Thread innerhalb von unsafePerformIO zu starten, erzeugt eine Fehlermeldung. 5.3.8 Einschränkungen des CHS Die gravierendste Einschränkung des CHS hängt mit der Redenition der IO-Monade zusammen. Module, die IO-Aktionen enthalten, können nur verwendet werden, wenn sie durch den Präprozessor verändert wurden. Dies funktioniert allerdings nur, wenn sie ihrerseits nur unterstützte IO-Aktionen verwenden. Für andere Module und solche, die nicht als Quelltext vorliegen, muss von Hand ein Art Wrapper erstellt werden. Eine weitere Einschränkung stellt die fehlende Unterstützung von asynchronen Exceptions dar. Möglicherweise lassen sich diese durch das Manipulieren des IO-Baums des betroenen Threads implementieren. Zusätzlich müssen dann jedoch auch die Aktionen block und unblock implementiert werden, die vorübergehend verhindern, dass asynchrone Exceptions empfangen werden bzw. den Empfang wieder erlauben. Die Aktion block lässt sich möglicherweise ähnlich wie denieren, die Existenz von von block unblock catch in Abschnitt 5.3.6.2 dürfte dies jedoch verkomplizieren. Das Fehlen macht sich auch jetzt schon bemerkbar, da die Aktion killThread be- reits implementiert ist, der Empfang dieser Nachricht jedoch nicht verhindert werden kann. 69 5 Debugging in Haskell 70 6 Debuggen von Transaktionen Bei dem hier vorgestellten Ansatz zum Debuggen von nebenläugen Programmen, die Transaktionen enthalten, wird auf den bereits im vorigen Kapitel vorgestellten Concurrent Haskell Debugger aufgebaut. Im Prinzip lassen sich Transaktionen fast genauso debuggen, wie die bisherigen nebenläugen Aktionen. Dazu wird einfach statt des Moduls Control.Concurrent.STM das vom CHD bereitgestellte Modul CHD.Control.Concurrent.STM importiert. Genauso wie bisher die MVars gibt es nun TVars, die der Kommunikation zwischen Threads dienen. Und durch das Ausführen von retry können Threads auch bei Transaktionen suspendieren, so wie dies bisher zum Beispiel durch das Lesen einer leeren MVar möglich war. Der eigentliche Programmablauf wird dabei nur durch das erfolgreiche Abschlieÿen einer Transaktion, eventuell mit einer Änderung von TVars, durch das Suspendieren des Threads oder durch das Auftreten einer Exception beeinusst. Um jedoch Programme zu debuggen, reicht es nicht unbedingt aus, nur die Eekte von Transaktionen zu betrachten. Da Transaktionen durchaus recht komplex sein können, ist es nicht trivial zu verstehen, wie diese Eekte zustande kommen. Daher ist es auch notwendig, die einzelnen Aktionen innerhalb einer Transaktion darzustellen. Der erweiterte Concurrent Haskell Debugger zeigt daher beides. Wie bisher werden im Hauptfenster die global sichtbaren Aktionen gezeigt. Zusätzlich zeigt ein weiteres Fenster die lokalen Aktionen, die nur innerhalb der Transaktion sichtbar sind. 6.1 Darstellung von globalen Aktionen Globale Aktionen sind solche, die sich direkt auf TVars auswirken oder direkt von diesen beeinusst werden. Dazu gehört das Verändern von Variablen am Ende einer Transaktion, das Lesen aus einer während der Transaktion noch nicht veränderten Variable, das Suspendieren auf eine oder mehrere Variable, aber auch das neu Starten einer Transaktion, nachdem eine bereits gelesene Variable von einem anderen Thread verändert wurde. All diese Aktionen werden im Hauptfenster dargestellt. Abbildung 6.1 zeigt das Hauptfenster des CHD beim Debuggen der STM-Version der dinierenden Philosophen aus Listing 4.6. Zu sehen sind auf der linken Seite die fünf Philosophen-Threads, auf der rechten Seite die TVars, die die Stäbchen repräsentieren. Da TVars nicht leer sein können, ist es hier besonders wichtig, dass ein Label einen Hinweis auf den Inhalt gibt. Hier sind das True und False, die angeben, ob ein Stäbchen auf dem Tisch liegt. 71 6 Debuggen von Transaktionen Abbildung 6.1: Screenshot des Hauptfensters des CHD: STM-Version der dinierenden Philosophen Im Folgenden werden die globalen Aktionen genauer erläutert. 6.1.1 Lesen einer TVar Das Lesen einer während der Transaktion noch nicht geschriebenen TVar ndet di- rekt statt. Daher wird die Leseaktion auch im Hauptfenster angezeigt. In Abbildung 6.1 führen die Threads 2 und 3 eine solche Leseaktion aus. Analog zum Lesen von MVars wird dies durch einen roten Pfeil und vor dem Lesen TVarReadSuspendA, danach durch TVarReadA dargestellt. durch das Aktionslabel 6.1.2 Suspendieren durch retry Wird ein Thread durch den Aufruf von eine der gelesenen TVars retry suspendiert, so geschieht dies, bis geändert wurde. Welche TVars dies sind, wird durch graue Pfeile dargestellt. Solange keine der Variablen geändert wurde, wird der Thread rot dargestellt. Wurde mindestens eine Variable geändert, so wechselt die Farbe auf gelb und die Transaktion kann von vorne begonnen werden. Währen der Thread suspendiert ist, wird 72 STMRetrySuspendA als Aktionslabel angezeigt, nachdem vom 6.1 Darstellung von globalen Aktionen Benutzer die Freigabe erteilt wurde wechselt es auf Thread 4 auf zwei TVars STMRetryA. In Abbildung 6.1 ist suspendiert. 6.1.3 Abschluss einer Transaktion Am Ende einer Transaktion werden die Schreibaktionen tatsächlich durchgeführt (commit ). Dass ein Thread die Schreibaktionen durchführen möchte, wird mit grünen Pfeilen auf die zu beschreibenden TVars dargestellt. Dabei wurde der abschlieÿende Test, ob die Sicht auf die gelesenen Variablen konsistent war, noch nicht durchgeführt. Schlieÿlich hat auch zu diesem Zeitpunkt ein anderer Thread noch die Möglichkeit, gelesene Variablen zu ändern. Daher ist das Aktionslabel STMTryCommitA. Thread 0 in Abb. 6.1 wird im nächsten Schritt seine Transaktion abschlieÿen. Konnte die Transaktion erfolgreich abgeschlossen werden, so werden die Labels der geschriebenen Variablen geändert, während der Transaktion erzeugte angezeigt und das Aktionslabel ändert sich auf STMCommitA. TVars im Hauptfenster 6.1.4 Neustart einer Transaktion bei inkonsistenter Sicht Wird bei der Konsistenzprüfung festgestellt, dass die Sicht auf die TVars nicht konsis- tent war, so wird dies durch einen orangen Pfeil auf die Variable, bei der dies zuerst festgestellt wurde, und das Aktionslabel STMInvalidSuspendA angezeigt (Thread 1 in Abb. 6.1). Dies bedeutet nicht, dass nicht noch andere Variablen geändert wurden, eine genügt jedoch, um die Transaktion neu zu starten. Erteilt der Benutzer die Freigabe, so wechselt das Aktionslabel auf STMInvalidA. 6.1.5 Propagieren einer Exception Wird eine Exception innerhalb einer Transaktion geworfen aber nicht abgefangen, so wirkt sich dies auch auf die globale Sicht auf das Programm aus. Zwar werden in diesem Fall alle durchgeführten Schreibaktionen verworfen, jedoch können durch eine Exception Referenzen auf während der Transaktion erzeugte TVars aus dem Bereich der Transaktion heraus gelangen. Daher werden in diesem Fall diese TVars auch im Hauptfenster angezeigt. Die Aktionslabel STMExceptionSuspendA STMExceptionA zeigen diesen Vorgang an. und 6.1.6 Entfernen von TVars Wird eine TVar im Programm nicht mehr referenziert, so wird diese genau wie ande- re Kommunikationsabstraktionen vom Garbage Collector entfernt. Genau wie diese werden daher auch TVars in diesem Fall zunächst rot markiert und nach kurzer Zeit aus der Anzeige gelöscht. 73 6 Debuggen von Transaktionen 6.2 Darstellung von lokalen Aktionen Lokale Aktionen sind solche, die sich nur auf die zur Transaktion lokale Sicht auswirken oder von dieser beeinusst werden. Dazu gehört das Erzeugen neuer Variablen, Schreibaktionen und das Lesen von bereits beschriebenen Variablen. Da die lokale Sicht für jeden Thread individuell ist, lässt sich diese nur schwer im Hauptfenster darstellen. Aus diesem Grund lässt sich über die Menüleiste ein Transaktionsfenster önen, das für jeden Thread, der sich gerade innerhalb einer Transaktion bendet, dessen lokale Sicht darstellt. Abbildung 6.2 zeigt dieses Fenster im gleichen Programmzustand wie Abb. 6.1. Abbildung 6.2: Screenshot des Transaktionsfensters des CHD Über die Karteireiter lässt sich auswählen, welcher Thread betrachtet werden soll. Hier ist Thread 0 ausgewählt. Ein Vergleich mit Abbildung 6.1 zeigt, dass sich die Werte der lokalen TVars 0 und 1 von denen im Hauptfenster unterscheiden. Dies liegt daran, dass die Schreibaktionen, die die Stäbchen zurücklegen, lokal bereits sichtbar sind, die Transaktion allerdings noch nicht abgeschlossen ist. Im Folgenden werden die lokalen Aktionen genauer erläutert. 6.2.1 Erzeugen einer neuer TVar Werden neue Transaktionsvariablen erzeugt, so sind diese zunächst nur innerhalb der Transaktion sichtbar. Daher werden diese auch im Transaktionsfenster dargestellt. TVar TVarNewA an- Wie bei anderen Kommunikationsabstraktionen wird das Erzeugen einer neuen durch einen blauen Pfeil und die Aktionslabels gezeigt. 74 TVarNewSuspendA und 6.2 Darstellung von lokalen Aktionen 6.2.2 Schreibaktionen Schreibaktionen werden für andere Threads erst nach dem erfolgreichen Abschluss der Transaktion sichtbar. Da sie sich jedoch auf den schreibenden Thread bereits vorher auswirken können, werden sie in dessen lokaler Sicht dargestellt. Dazu wird TVar im Transaktionsfenster erzeugt und das Schreiben durch einen grünen Pfeil und die Aktionslabels TVarWriteLocalSuspendA und TVarWriteLocalA angezeigt. eine lokale Kopie der zu beschreibenden 6.2.3 Lesen einer bereits geschriebenen TVar Wird eine während der Transaktion bereits geschriebene Transaktionsvariable wieder gelesen, so soll nicht der global sichtbare, sondern der geschriebene Wert zurückgeliefert werden. Daher wird das Lesen in diesem Fall zwar durch einen roten Pfeil, jedoch im Transaktionsfenster und mit den Aktionslabels und TVarReadLocalA TVarReadLocalSuspendA angezeigt. 6.2.4 Alternative Ausführung mit orElse orElse-Blocks STMOrElseSuspendA und STMOrElseA anein retry ausgeführt, so werden die darin Um dem Benutzer anzuzeigen, dass als nächstes der erste Teil eines ausgeführt wird, werden die Aktionslabels gezeigt. Wird innerhalb dieses Blocks durchgeführten Schreibaktionen rückgängig gemacht, indem im Transaktionsfenster die alten Werte in die Transaktionsvariablen zurückgeschrieben werden. Angezeigt wird dies durch grüne Pfeile und die Aktionslabels STMOrElseRetryA. STMOrElseRetrySuspendA und Wurden neue Transaktionsvariablen erzeugt, so werden diese rot markiert und verschwinden nach kurzer Zeit. 6.2.5 Erzeugen und Testen von Invarianten alwaysSucceeds oder always erzeugt, so wird dies STMASucceedsA bzw. STMAlwaysA angezeigt. Wurden die in Wird eine neue Invariante durch durch die Aktionslabels der Invariante durchgeführten Aktionen erfolgreich abgeschlossen, so müssen wie bei orElse die durchgeführten Schreibaktionen rückTVars entfernt werden. Dies wird durch die Aktionslabels STMASuccessSuspendA und STMASuccessA angezeigt. einem retry im ersten Teil von gängig gemacht und neue Vor dem Abschluss einer Transaktion müssen eventuell vorhandene neue und mit geänderten TVars assoziierte Invarianten getestet werden. Dass dieser abschlieÿen- STMFinalInvarChecksA angezeigt. ZusätzInvariante das Aktionslabel STMInvarCheckA ange- de Test durchgeführt wird, wird durch lich wird vor jeder einzelnen zeigt. Auch hier müssen nach jedem einzelnen Test die durchgeführten Änderungen 75 6 Debuggen von Transaktionen verworfen werden. Dabei werden die Labels STMInvarCheckSuccessA STMInvarCheckSuccessSuspendA und angezeigt. 6.2.6 Abfangen von Exceptions Wird eine Exception noch innerhalb einer Transaktion abgefangen, so wird dies durch die Aktionslabels STMCatchSuspendA die Schreibaktionen, die innerhalb des STMCatchA angezeigt. Auch hier werden catch-Blocks ausgeführt wurden, rückgängig und gemacht. Neu erzeugte Transaktionsvariablen bleiben jedoch erhalten, da Referenzen auf diese durch die Exception erhalten geblieben sein könnten. 6.3 Überspringen der Transaktionsschritte Ist man nicht an den einzelnen Schritten einer Transaktion interessiert, ist es ziemlich aufwändig, dem Thread durch einen Mausklick immer wieder die Freigabe zur weiteren Ausführung zu erteilen. Zwar kann man dem Thread eine generelle Ausführungsfreigabe erteilen, um Mausklicks zu sparen, muss dann aber auch aufpassen, dass man den Thread auch rechtzeitig wieder stoppt. Um dieses Problem zu lösen, wurde der Menüleiste die Option Step Transactions hinzugefügt. In der Voreinstellung ist diese Option aktiviert und alle Transaktionsschritte werden angezeigt. Wird die Option deaktiviert, so werden nur Aktionen angezeigt, die den Zustand des nebenläugen Programms verändern, also der erfolgreiche Abschluss, das Suspendieren oder das Propagieren einer Exception der Transaktion. 6.4 Transparente STM-Bibliothek Wie bereits erwähnt, ist es bei Transaktionsvariablen besonders wichtig, dass sich der Inhalt durch ein Label anzeigen lässt. Wie bei den Aktionen aus der ConcurrentBibliothek gibt es auch für Schreibaktionen aus der STM-Bibliothek des CHD Aktionen, die zusätzlich ein Label als Argument erhalten. Damit auch hier das Programm nicht angepasst werden muss, wenn der CHD nicht verwendet werden soll, existiert das Modul CHD.Control.ConcurrentLess, das zusätzlich zu allen Aktionen Label-Zusatz enthält. Diese ignorieren der STM-Bibliothek auch diejenigen mit dem einfach das übergebene Label. 76 7 Implementierung des Debuggers für Transaktionen Die naheliegende Idee, Transaktionen genauso zu debuggen wie dies bisher bei den nebenläugen Aktionen geschieht, also die Originalaktionen zu benutzen und einfach vor und nach deren Ausführung eine Nachricht an den Debugger zu senden, ist nicht oder zumindest nur mit Einschränkungen möglich. So hätte man zum Beispiel keine Möglichkeit, eine Nachricht an den Debugger zu senden und ihn damit anzuhalten, kurz bevor eine Transaktion abgeschlossen wird. Es ist zwar möglich, am Ende der eigentlichen Transaktion eine Nachricht zu senden, aber danach ndet möglicherweise noch die Überprüfung von Invarianten statt. Und selbst wenn dieses Problem gelöst oder als nebensächlich eingestuft würde, so tut sich die nächste Schwierigkeit auf. Es ist zwar wieder einfach, durch Nachrichten festzustellen, wann ein retry aufge- rufen wird, und auch, wann der Thread wieder weiter ausgeführt werden kann. Um jedoch anzeigen zu können, auf welche TVars der Thread suspendiert ist, muss sich der Debugger merken, welche Transaktionsvariablen bisher gelesen wurden. Überlegt man in diese Richtung weiter, so realisiert man schnell, dass der Debugger die Transaktionsmechanismen simulieren muss, um Transaktionen richtig anzeigen zu können. Aus diesen Gründen wurde für das Debuggen von Transaktionen ein anderer Ansatz gewählt. Statt die Transaktionsbibliothek des ghc zu benutzen, wird die in Abschnitt 4.2 vorgestellte Lightweight-Bibliothek genutzt. Die zum Debuggen benötigten Informationen sind so entweder direkt verfügbar oder lassen sich vergleichsweise leicht verfügbar machen. 7.1 Erweiterung des Debuggers Da das Debuggen von Transaktionen auf den Concurrent Haskell Debugger aufbauen soll, ndet auch die Kommunikation der Threads mit dem Debugger durch Nachrichten über den debugMsgChan statt. Wann welche Nachricht gesendet und wie die dazu nötigen Informationen gewonnen werden, wird im Folgenden erläutert. Ein groÿer Teil des Quellcodes der STM-Bibliothek des erweiterten CHD ist der bereits in Abschnitt 4.2 vorgestellten Implementierung recht ähnlich, daher wird hier hauptsächlich auf die relevanten Änderungen eingegangen. Um auch bei Transaktionen die aktuelle Stelle im Quellcode markieren und bei Bedarf auch zusätzliche Informationen anzeigen zu können, wurden für alle STM-Aktionen genau wie schon für die 77 7 Implementierung des Debuggers für Transaktionen Concurrent-Bibliothek Aktionen eingeführt, die zusätzlich Quellcodeinformationen und, wo sinnvoll, auch ein Label als Argument erwarten. 7.1.1 Schreiben und Lesen einer TVar writeTVar wird immer nur lokal durchgeführt. Daher genügt es, je mit der Id der TVar und gegebenenfalls Quellcodeinformationen und Die STM-Aktion eine Nachricht Label vor und nach der Ausführung dieser Aktion an den Debugger zu senden. Etwas mehr zu beachten gibt es bei der Aktion auf die global sichtbare ab, ob die TVar TVar readTVar. Ob diese im Hauptfenster oder vielleicht eine lokale Kopie zugreift, hängt davon während der Transaktion bereits geschrieben wurde. Hier zeigt sich nun schon der Vorteil, den die Nutzung der Lightweight-Bibliothek mit sich bringt. So muss sich nicht der Debugger merken, welche Transaktionsvariablen von welcher Transaktion bereits geschrieben wurden, sondern diese Information steht durch das Feld writtenTVars im Transaktionszustand direkt zur Verfügung. So lassen sich, wie in Listing 7.1, in den verschiedenen Fällen unterschiedliche Nachrichten senden. readTVarLine :: CodePosition -> TVar a -> STM a readTVarLine pos ( TVar tVarRef id waitQ invs ) = STM (\ stmState -> do let isLocal = elem id ( newTVars stmState ) || elem id ( map fst ( writtenTVars stmState )) debugStop1 <- sendDebugMsg ( if isLocal then ( TVarReadLocalSuspend id pos ) else ( TVarReadSuspend id pos )) takeMVar debugStop1 -- Hier wird die TVar wie bisher ausgelesen und der veraenderte -- Transaktionszustand erzeugt debugStop2 <- sendDebugMsg ( if isLocal then TVarReadLocal id pos else TVarRead id pos ) takeMVar debugStop2 return ( Success newState val )) Listing 7.1: Nachrichten an den Debugger in readTVar Anzumerken ist hier noch, dass lokales Lesen nicht nur bei bereits beschriebenen TVars, sondern auch bei neu erstellten wird ein neues Feld newTVars TVars stattndet. Um dies zu ermöglichen, im Transaktionszustand benötigt, das bisher nicht ge- braucht wurde. Grund dafür ist, dass sich im Falle von neu erstellten Transaktionsvariablen die Implementierung von der Anschauung und damit auch der Darstellung im CHD unterscheidet. Anschaulich bleiben neue TVars solange lokal, bis sie durch den Abschluss einer Transaktion oder eine Exception auch auÿerhalb der Transaktion 78 7.1 Erweiterung des Debuggers sichtbar sind. Tatsächlich werden diese allerdings direkt erzeugt und gegebenenfalls vom Garbage Collector wieder entfernt. 7.1.2 Erzeugen einer neuen TVar Nachdem im vorigen Abschnitt das Erzeugen von neuen TVars bereits kurz angerissen wurde, folgen hier nun die Details (siehe Listing 7.2). Wie wohl bereits erwartet, wird auch hier vor und nach der Erzeugung je eine Nachricht gesendet. Wie bereits bei den anderen Kommunikationsabstraktionen kann sich auch hier der Debugger um die Vergabe der Ids kümmern, indem bei der ersten Nachricht eine MVar an den Debugger gesendet wird, aus der dann die Id ausgelesen wird. newTVarLabelLine :: CodePosition -> String -> a -> STM ( TVar a ) newTVarLabelLine pos label v = STM (\ stmState -> do returnNewNoMVar <- newEmptyMVar debugStop1 <- sendDebugMsg ( TVarNewSuspend returnNewNoMVar label pos ) takeMVar debugStop1 id <- readMVar returnNewNoMVar newTVarVal <- newIORef v newTVarRef <- newMVar newTVarVal -- Hier werden wie bisher die Listen fuer Invarianten und fuer das -- Warten auf die TVar nach einem retry erzeugt let tVar = ( TVar newTVarRef id newWaitQ invarList ) debugStop2 <- sendDebugMsg ( TVarNew id label pos ) takeMVar debugStop2 addFinalizer newTVarRef ( do { sendDebugMsg ( TVarDied id ); return ()}) let newState = stmState { newTVars = id : newTVars stmState , keepNewTVars = isEmptyMVar newTVarRef >> keepNewTVars stmState } return ( Success newState tVar )) Listing 7.2: Implementierung von newTVar für den CHD Auch Transaktionsvariablen werden, wenn sie nicht mehr gebraucht werden, vom Garbage Collector aus dem Speicher entfernt. Eine TVar wird dann entfernt, wenn MVar, die den Wert enthält, aus dem Speicher entfernt wird. Dies lässt sich daher MVars bereits geschieht. Die IO-Aktion, die die Nachricht sendet, wird mit addFinalizer mit der MVar assoziiert, die den Wert der TVar enthält. Allerdings ergibt sich im Fall von Transaktionen aus diesem die genauso an den Debugger melden, wie dies bei Vorgehen ein Problem. Wird die Transaktion abgeschlossen, so werden die Ids der neu erzeugten Transaktionsvariablen an den Debugger gesendet, damit diese im Hauptfenster angezeigt werden. Leider lässt sich jedoch nicht feststellen, ob Referenzen auf diese TVars tatsächlich aus der Transaktion herausgelangen oder diese nur innerhalb der Transaktion verwendet wurden. Im letzteren Fall könnte der Garbage Collector eine TVar bereits aus dem Speicher entfernen, bevor die Transaktion abgeschlossen 79 7 Implementierung des Debuggers für Transaktionen ist. Dadurch würde die Nachricht zum Entfernen der Transaktionsvariablen an den Debugger gesendet, bevor diese im Hauptfenster angezeigt würde. Dort würde sie nun ungenutzt angezeigt, bis der Debugger beendet wird. Um dieses Problem zu umgehen, könnte sich der Debugger zum Beispiel merken, welche Transaktionsvariablen bereits entfernt wurden. Hier wurde jedoch aufgrund des geringeren Aufwands eine andere Strategie gewählt. Um zu verhindern, dass neue TVars zu früh aus dem Speicher entfernt werden, werden künstliche Referenzen auf diese im Transaktionszustand gehalten. Beim Abschluss der Transaktion werden diese Referenzen dann mit an den Debugger gesendet und erst, wenn dieser die Referenzen verwirft, kann der Garbage Collector die TVar entfernen. Aufgrund von Haskells Typsystem können die Referenzen nicht direkt zum Beispiel in einer Liste vorgehalten werden. Daher werden diese indirekt in einer IO-Aktion im Feld vorgehalten. Die möglicherweise verlängerte Lebenszeit der keepNewTVars TVars spielt für den Pro- grammablauf keine Rolle und ist daher unproblematisch. 7.1.3 orElse Wird während einer Transaktion im ersten Teil eines orElse-Blocks ein retry aufge- rufen, so werden die dort durchgeführten Änderungen verworfen. Da die Schreibaktionen bei der Implementierung der lightweight STM-Bibliothek nur gesammelt und erst am Ende ausgeführt werden, ist dies nicht weiter schwierig. Es werden einfach die Aktionen so weiterverwendet, wie sie vor Ausführung des ersten orElse-Blocks waren. Durch die Darstellung im CHD sieht die Situation jedoch ganz anders aus. Wurden im ersten Teil des orElse-Blocks Schreibaktionen durchgeführt, so werden diese im Transaktionsfenster des Threads auch angezeigt. Daher müssen die dort angezeigten Werte explizit wieder angepasst werden. Dazu muss nicht nur wie bisher nachvollziebar sein, welche nerhalb des orElse-Blocks TVars geändert wurden, sondern auch, ob dies in- stattgefunden hat oder schon vorher. Auÿerdem muss sich auch der Wert oder vielmehr das Label, das vor der Ausführung des orElse Blocks angezeigt wurde, bestimmen lassen. Um dies zu erreichen, bekommt das Feld writtenTVars des Transaktionszustandes einen neuen Typ (siehe Listing 7.3), der auch die Labels enthält. Auch das Feld dem die TVar newTVars erhält zusätzlich das Label, mit erzeugt wurde. writtenTVars als verschachtelte Liste ist es orElse-Blocks eine leere Liste vorne anzuhängen. Durch die Implementierung des Feldes möglich, beim Betreten des ersten In diese Liste werden die Schreibaktionen eingetragen, die innerhalb dieses Blocks stattnden. Die bisher vorgestellten STM-Aktionen, die die Felder oder newTVars writtenTVars benutzen, müssen natürlich entsprechend angepasst werden. Liefert die Ausführung der ersten Transaktion in orElse nun das Ergebnis Retry, so muss ermittelt werden, welche Transaktionsvariablen geändert wurden. Zuerst werden durch die Funktion getDelLocals die TVars bestimmt, die komplett aus dem Transaktionsfenster gelöscht werden können. Dies sind zunächst natürlich diejenigen, 80 7.1 Erweiterung des Debuggers data StmState = TST { writtenTVars :: [[( TVarNo , String , MVar ( IORef [ Invariant ]))]] , newTVars :: [( TVarNo , String )] , ...} orElseLine :: CodePosition -> STM a -> STM a -> STM a orElseLine pos ( STM stm1 ) ( STM stm2 ) = STM (\( stmState@TST {... , writtenTVars = fWritten , newTVars = fnew , keepNewTVars = fkeep }) -> do debugStop1 <- sendDebugMsg ( STMOrElseSuspend pos ) takeMVar debugStop1 debugStop2 <- sendDebugMsg ( STMOrElse pos ) takeMVar debugStop2 stm1Res <- stm1 stmState { writtenTVars = []: fWritten } case stm1Res of Retry newState@TST {... , writtenTVars = nWritten , readTVars = nRead , newTVars = nNew } retryPos -> do let delLocals = getDelLocals fnew nNew nWritten restoreLocals = getRestLocals fnew nWritten debugStop3 <- sendDebugMsg ( STMOrElseRetrySuspend delLocals ( fst restoreLocals ) ( snd restoreLocals ) retryPos ) takeMVar debugStop3 debugStop4 <- sendDebugMsg ( STMOrElseRetry delLocals ( fst restoreLocals ) ( snd restoreLocals ) retryPos ) takeMVar debugStop4 stm2 newState {... , newTVars = fnew , keepNewTVars = fkeep } Success newState@TST { writtenTVars = writ1 : writ2 : restWritten } res -> return ( Success newState { writtenTVars = ( writ1 ++ writ2 ): restWritten } res ) Exception newState@TST { writtenTVars = writ1 : writ2 : restWritten } e -> return ( Exception newState { writtenTVars = ( writ1 ++ writ2 ): restWritten } e) Listing 7.3: Implementierung von orElse für den CHD 81 7 Implementierung des Debuggers für Transaktionen die innerhalb des Blocks erzeugt wurden, aber auch die Transaktionsvariablen, die nur innerhalb dieses Blocks geändert wurden, denn nach dem Wiederherstellen gelten für diese Transaktionsvariablen wieder die Werte, die im Hauptfenster angezeigt werden. Die Funktion getRestLocals ermittelt die Ids und Labels der Transaktionsvariablen, deren Labels im Transaktionsfenster angepasst werden müssen, also alle, die innerhalb des ersten orElse Blocks, aber auch schon davor verändert oder neu erzeugt wurden. Das neue Label ist dann das, das beim ersten Auftreten der jeweiligen in der Restliste von writtenTVars TVar eingetragen ist. Wie die beiden Funktionen recht kurz als list comprehensions deniert werden können, zeigt Listing 7.4. getDelLocals fNew nNew nWritten = [ t | t <- map fst nNew , t ` notElem ` map fst fNew ] ++ [ t | t <- ( map fst3 ( head nWritten )) , t ` notElem ` ( map fst3 ( concat ( tail nWritten ))) , t ` notElem ` ( map fst nNew )] getRestLocals fnew nWritten = unzip ([( t , l ) | (t ,l , _ ) <- nubBy (\ t1 t2 -> fst3 t1 == fst3 t2 ) ( concat ( tail nWritten )) , t ` elem ` map fst3 ( head nWritten )] ++ [( t , l )| (t , l ) <- fnew , t ` elem ` map fst3 ( head nWritten ) , t ` notElem ` map fst3 ( concat ( tail nWritten ))]) Listing 7.4: Ermitteln von zu entfernenden und wieder herzustellenden lokalen TVars Die so gewonnenen Informationen werden dann an den Debugger gesendet, der die Darstellung im Transaktionsfenster entsprechend anpasst. Bei dieser Implementierung wurde angenommen, dass sich alle Schreibaktionen, die orElse-Blocks ausgeführt wurden, in der ersten Liste des Felds writtenTVars benden. Dies ist jedoch nicht unbedingt gewährleistet. Benden sich innerhalb dieses Blocks weitere verschachtelte Aufrufe von orElse, so erzeugen diese innerhalb des ersten selbst wieder eine neue Liste in diesem Feld. Um die geforderte Eigenschaft zu ge- writtenTVars konkateniert werden, Success oder Exception liefert. währleisten, müssen die ersten beiden Listen in falls die Ausführung des Blocks das Ergebnis 7.1.4 catchSTM Wird eine Exception noch innerhalb einer Transaktion durch so werden nur die innerhalb des catchSTM-Blocks catchSTM gefangen, durchgeführten Schreibaktionen verworfen. Dort neu erzeugte Transaktionsvariablen müssen erhalten bleiben, da Referenzen auf diese durch die Exception erhalten geblieben sein können. Welche 82 7.1 Erweiterung des Debuggers Transaktionsvariablen in der lokalen Darstellung angepasst und welche daraus gelöscht werden können, wird mit den gerade erwähnten Anpassungen wie bei orElse ermittelt. 7.1.5 Erzeugen von Invarianten Werden neue Invarianten mit alwaysSucceeds oder always erzeugt, passiert nahezu dasselbe. Es werden jedoch unterschiedliche Nachrichten gesendet. Daher wird der doCheck ausgegliedert. Genau wie bei orElse doCheck durchgeführten Änderungen wieder rückgängig gemacht wer- eigentliche Code in die STM-Aktion müssen die in den. Also wird auch hier vor der Ausführung eine leere Liste vorne an die Liste im Feld writtenTVars angehängt. Welche Änderungen wieder rückgängig gemacht werden, wird dann mit den Funktionen aus Listing 7.4 bestimmt. Auch hier muss wieder auf Verschachtelung geachtet werden. Da hier kaum ein Unterschied zur Implementierung in orElse besteht, ist in Listing 7.5 nur der Typ von doCheck gezeigt. Erwähnenswert sind noch zwei Dinge: Zum einen wurde der Datentyp von Invarianten um die Quellcodeposition und ein Label zur leichteren Identizierung der Invariante erweitert, um später bei der abschlieÿenden Invariantenprüfung sinnvollere Angaben machen zu können. Zum anderen gibt doCheck sowohl die Ids der TVars, die aus dem Transaktionsfenster gelöscht werden sollen, als auch die Ids und neuen Labels derjenigen, die geändert werden sollen, zurück. Nun lässt sich die Aktion always recht leicht implementieren. Dazu wird das Senden der Nachrichten in zwei STM-Aktionen verpackt, die dann vor und nach der eigentlichen Erzeugung der Invarianten ausgeführt werden. Dabei wird schon von der Implementierung des (>>=)-Kombinators garantiert, dass die Nachrichten am Ende, die den Erfolg der Invariantenprüfung anzeigen, nur dann gesendet werden, wenn die Invariantenprüfung auch tatsächlich das Resultat werden auch die von doCheck Success liefert. In diesem Fall gelieferten Informationen zur Wiederherstellung des Zustands vor der Invariantenerzeugung genutzt und an den Debugger gesendet. Mit anderen Nachrichtennamen und ohne das so auch die Aktion alwaysSucceeds assert im Argument von doCheck, kann implementiert werden. 7.1.6 atomically In den vorherigen Abschnitten wurde gezeigt, wie die einzelnen STM-Aktionen geändert wurden, um die Kommunikation mit dem Debugger zu ermöglichen. Nun muss nur noch erläutert werden, wie die Kommunikation zu Beginn und beim Abschluss einer Transaktion funktioniert. Zu Beginn wird einfach eine Nachricht an den Debugger gesendet, die diesem mitteilt, dass eine Transaktion beginnt. Am Ende der Transaktion werden gegebenenfalls noch Invarianten geprüft. Eingeleitet wird dies mit der Nachricht STMFinalInvarChecks und bei jeder zu überprüfenden Invariante always geschieht, werden sehr ähnliche Nachrichten verschickt, wie dies schon bei 83 7 Implementierung des Debuggers für Transaktionen data Invariant = Invariant ID ( IO ()) ( STM ()) CodePosition String doCheck :: CodePosition -> String -> STM () -> STM ([ TVarNo ] ,([ TVarNo ] ,[ String ])) alwaysLabelLine :: CodePosition -> String -> STM a -> STM () alwaysLabelLine pos label stm = do STM (\ stmState -> do debugStop1 <- sendDebugMsg ( STMAlways label pos ) takeMVar debugStop1 return ( Success stmState ())) ( del ,( restNo , restLabel )) <- doCheck pos label ( stm > >= ( flip assert ) ( return ())) STM (\ stmState -> do debugStop2 <- sendDebugMsg ( STMAlwaysSuccessSuspend del restNo restLabel label pos ) takeMVar debugStop2 debugStop3 <- sendDebugMsg ( STMAlwaysSuccess del restNo restLabel label pos ) takeMVar debugStop3 return ( Success stmState ())) Listing 7.5: Implementierung von always für den CHD mit dem Unterschied, dass zur Darstellung des Quelltextes die Information aus der Invariante genutzt wird. Interessanter ist da schon, was am Schluss bei den verschiedenen Resultaten Success, Retry und Exception geschieht. 7.1.6.1 Success Wird als Resultat Success (Listing 7.6) zurückgeliefert, so werden die geplanten Schreibaktionen im Hauptfenster zunächst einmal angezeigt. Welche Transaktionsvariablen geschrieben oder neu erzeugt werden und welche Label dazugehören, kann writtenTVars und newTVars des Transaktionszustands geleTVar während einer Transaktion mehr als einmal beschrieben wurde, so interessiert hier natürlich nur der letzte Wert. Durch die Funktion nubBy lassen sich für jede TVar alle anderen Werte aus der Liste entfernen. Wichtig ist, dass leicht aus den Feldern sen werden. Falls eine keine Nachricht an den Debugger gesendet wird, solange der Thread das globale Lock besitzt. Dies hat den Vorteil, dass alle anderen Threads ihre Transakionen weiterhin durchführen können, auch während ein Thread durch den Debugger gestoppt ist. Dies bedeutet jedoch auch, dass immer noch ein anderer Thread eine der gelesenen Transaktionsvariablen ändern kann und die Transaktion damit nicht mehr konsistent ist, nachdem die geplanten Schreibaktionen angezeigt wurden. Wird durch die Validitätsprüfung bestätigt, dass die Änderungen durchgeführt werden können, so wird dies getan. Eine Nachricht an den Debugger veranlasst diesen, dies auch darzustellen. An dieser Stelle wird dann auch die IO-Aktion, die die Refe- 84 7.1 Erweiterung des Debuggers renzen auf die neu erzeugten Transaktionsvariablen aufrechterhält, mit an den Debugger gesendet, damit der Garbage Collector diese erst nach erfolgter Darstellung aus dem Speicher entfernen kann. Wird jedoch festgestellt, dass die Transaktion nicht valide war, so wird auch dies als Nachricht an den Debugger gesendet. Da der Benutzer möglicherweise an dem Grund für die Inkonsistenz interessiert ist, wird die Konsistenzprüfung so geändert, dass sie nicht nur einen boolschen Wert, sondern zusätzlich die Id der Transaktionsvariablen zurückliefert, bei der die Inkonsistenz zuerst nachgewiesen wurde. So kann der Debugger anzeigen, welche Transaktionsvariable nach dem Lesen noch geändert wurde. Da die Nachrichten bei Inkonsistenz auch bei den Resultaten benötigt werden, wurden sie in die Aktion stopInvalid Retry und Exception ausgelagert. Success newState res -> do let written = unzip (( nubBy (\ t1 t2 -> fst t1 == fst t2 ) ( map (\( a ,b , _ ) -> (a , b )) ( concat ( writtenTVars newState ))) ++ newTVars newState ) commitStop1 <- sendDebugMsg ( STMCommitSuspend ( fst written ) ( snd written ) pos ) takeMVar commitStop1 takeMVar globalLock ( valid , id ) <- ( isValid newState ) if valid then do -- Hier werden die Aktionen zum Abschluss ausgefuehrt putMVar globalLock () commitStop2 <- sendDebugMsg ( STMCommit ( fst written ) ( snd written ) ( keepNewTVars newState ) pos ) takeMVar commitStop2 return res else do putMVar globalLock () stopInvalid id atomically stmAction stopInvalid :: TVarNo -> CodePosition -> IO () stopInvalid id pos = do invalidStop1 <- sendDebugMsg ( STMInvalidSuspend id pos ) takeMVar invalidStop1 invalidStop2 <- sendDebugMsg ( STMInvalid id pos ) takeMVar invalidStop2 Listing 7.6: Nachrichten an den Debugger nach erfolgreicher Transaktionsausführung 85 7 Implementierung des Debuggers für Transaktionen 7.1.6.2 Retry Wird während der Transaktion auÿerhalb von orElse ein retry aufgerufen, so wird die Transaktion erst dann neu gestartet, wenn eine der gelesenen TVars geändert wurde. Um dies anzeigen zu können, enthält die Nachricht an den Debugger alle gelesenen Transaktionsvariablen (siehe Listing 7.7). Da es auch sein kann, dass TVars gelesen wurden, die erst während der Transaktion erstellt wurden, es aber keinen Sinn macht, auch auf diese zu warten, werden diese zuvor aus der Liste entfernt. Retry newState retryPos takeMVar globalLock ( valid , id ) <- isValid if valid then do wait newState let allRead = nub read = filter -> do newState ( map fst ( concat ( readTVars newState ))) ( not .( ` elem ` ( map fst ( newTVars newState )))) allRead retryStop1 <- sendDebugMsg ( STMRetrySuspend read retryPos False ) putMVar globalLock () takeMVar retryStop1 tryRetry read ( retryMVar state ) retryPos retryStop2 <- sendDebugMsg ( STMRetry read retryPos ) takeMVar retryStop2 atomically stmAction else do putMVar globalLock () stopInvalid id pos atomically stmAction tryRetry read wait pos = do empty <- isEmptyMVar wait if empty then do takeMVar wait retryStop <- sendDebugMsg ( STMRetrySuspend read pos True ) takeMVar retryStop else do takeMVar wait Listing 7.7: Nachrichten an den Debugger beim Neustart durch retry Wird hier nach der ersten Nachricht an den Debugger auf die Freigabe gewartet, so kann diese auch gewährt werden, bevor der Thread tatsächlich fortfahren kann, da dieser noch auf die Änderung mindestens einer der gelesenen Transaktionsvariablen warten muss. Würde nun direkt auf die retryMVar gewartet, so würde der Thread sofort von selbst fortfahren, wenn diese durch den Abschluss einer anderen Transaktion mit einem Wert gefüllt wird. Um dies zu verhindern und damit dem Benutzer 86 7.1 Erweiterung des Debuggers mehr Kontrolle zu geben, wird durch Aufruf von tryRetry nach der Freigabe zuerst überprüft, ob der Thread direkt weiter ausgeführt werden kann. Ist dies nicht der Fall, so wird nach dem Suspendieren auf die retryMVar eine weitere Nachricht an den Debugger gesendet und dadurch auf eine erneute Freigabe gewartet. Der Boolsche Wert in der Nachricht gibt dabei an, ob der Thread weiter ausgeführt werden kann. Der Debugger interpretiert dies, indem er den Thread rot oder gelb färbt. 7.1.6.3 Exception Wird innerhalb einer Transaktion eine nicht abgefangene Exception geworfen, so werden die Schreibaktionen verworfen. Während der Transaktion erzeugte Transaktionsvariablen müssen jedoch im Hauptfenster dargestellt werden, da sich nicht feststellen lässt, ob mit der Exception nicht auch Referenzen auf diese dem Bereich der Transaktion herausgelangen. Die Ids und Labels dieser TVars aus TVars wer- den daher auch hier als Nachricht an den Debugger gesendet. Da wohl nur in seltenen Fällen tatsächlich Referenzen durch eine Exception nach auÿen transportiert werden, ist es hier besonders wichtig, dass die Nachrichten über die Entfernung aus dem Speicher nicht zu früh gesendet werden. Daher werden mit der zweiten Nachricht auch hier die Referenzen auf die neuen TVars an den Debugger gesendet. Exception newState e -> do takeMVar globalLock ( valid , id ) <- isValid newState putMVar globalLock () if valid then do let newVars = unzip ( newTVars newState ) exceptionStop1 <- sendDebugMsg ( STMExceptionSuspend e ( fst newVars ) ( snd newVars ) pos ) takeMVar exceptionStop1 exceptionStop2 <- sendDebugMsg ( STMException e ( fst newVars ) ( snd newVars ) ( keepNewTVars newState ) pos ) takeMVar exceptionStop2 Control . Exception . throw e else do stopInvalid id pos atomically stmAction Listing 7.8: Nachrichten an den Debugger beim Propagieren einer Exception 7.1.7 Unterbinden von Nachrichten an den Debugger Wie in Abschnitt 6.3 erläutert wurde, ist es möglich zu verhindern, dass Zwischenschritte bei Transaktionen angezeigt werden. Eine Möglichkeit, dies zu erreichen 87 7 Implementierung des Debuggers für Transaktionen ist, den Debugger so zu ändern, dass nur relevante Nachrichten angezeigt werden. Die etwas ezientere Möglichkeit, die hier gewählt wurde, ist, Nachrichten bei Zwischenschritten erst gar nicht zu senden. Dies wird erreicht, indem ein neues Feld showTransActions vom Typ Bool die Information enthält, ob jede Nachricht ge- sendet werden soll oder nicht. Nun muss jedoch noch geklärt werden, wie diese Information vom Debugger zur Transaktion kommuniziert wird. Dazu kann nun aber ausgenutzt werden, dass bereits eine Kommunikation in diese Richtung stattndet. Immer wenn ein Thread eine Nachricht an den Debugger sendet, wartet er danach darauf, dass dieser eine Nachrichten über diese MVar mit dem Wert () füllt. Stattdessen können jedoch auch MVar gesendet werden. Listing 7.9 zeigt, wie dies geschieht. ... -- oldState ist der alte Transaktionszustand debugStop <- sendDebugMsg anyMsg StepTransactions doStep <- takeMVar debugStop let newState = oldState { showTransActions = doStep } ... Listing 7.9: Nachrichten vom Debugger an den Thread Bisher werden solche Nachrichten nur für diesen einen Zweck verwendet. Möglicherweise gibt es jedoch noch andere sinnvolle Anwendungen. Ein Problem existiert jedoch noch. Wenn eine neue Transaktionsvariable erzeugt wird, so wird die neue TVarNo vom Debugger vergeben. In diesem Fall muss also auf jeden Fall eine Nachricht gesendet werden. Damit das Erzeugen einer neuen TVar nicht angezeigt wird, wenn die Anzeige von Zwischenaktionen abgeschaltet ist, wird in diesem Fall eine andere Nachricht gesendet (siehe Listing 7.10). Bei dieser Nachricht wird nichts dargestellt und die Freigabe vom Debugger automatisch erteilt. ... stopMsg <- sendDebugMsg ( TVarGetNewNo ( CHD returnNewNoMVar )) takeMVar stopMsg ... Listing 7.10: Erfragen einer neuen TVarNo beim Debugger 7.2 Erweiterung des Steppers Damit auch weiterhin nach Deadlocks und inzwischen auch nach nicht abgefangenen Exceptions gesucht werden kann, müssen auch Transaktionen in die IO-Struktur des CHS eingepasst werden. Dies ist besondere deshalb wichtig, da durch die Möglichkeit, einen Thread durch den Aufruf von retry zu blockieren, auch Transaktionen am Auftreten eines Deadlocks beteiligt sein können. So lässt sich die STM-Version der 88 7.2 Erweiterung des Steppers dinierenden Philosophen aus Listing 4.6 leicht so verändern, dass wieder ein Deadlock auftreten kann. Dazu müssen lediglich, wie in Listing 7.11, die Stäbchen in separaten Transaktionen vom Tisch genommen werden. phil left right = do atomically ( takeStick left ) atomically ( takeStick right ) atomically ( do putStick left putStick right ) phil left right Listing 7.11: STM-Version der dinierenden Philosophen mit Deadlock Im Folgenden wird erläutert, wie Transaktionen in die Deadlocksuche einbezogen werden können, wie auch dabei der Suchraum eingeschränkt werden kann und wie die Integration in den CHD gelingt. 7.2.1 Deadlocksuche mit Transaktionen Wie schon bei der Darstellung von Transaktionen im CHD, macht es auch bei der Deadlocksuche Sinn, nicht die STM-Aktionen aus der Originalbibliothek, sondern auch hier die Lightweight-Bibliothek zu verwenden. So ist es zum Beispiel nicht trivial, ein echtes Blockieren des Threads beim Aufruf von retry zu verhindern oder die Auswirkungen einer Transaktion rückgängig zu machen. Unabhängig von der Implementierung kann zunächst überlegt werden, dass eine Transaktion semantisch gesehen eine einzige atomare Aktion darstellt. Daher kann ConcAction im IO-Baum des Threads repräsentiert ConcActions ist, dass sie sich wieder rückgängig machen las- eine Transaktion auch als eine werden. Wichtig an den sen. An dieser Stelle hilft die Implementierung der Lightweight-Bibliothek. Wird eine Transaktionsvariable während einer Transaktion zunächst geschrieben und dann wieder gelesen, so werden alle bisherigen Schreibaktionen zunächst ausgeführt und dann durch eine restore -Aktion wieder rückgängig gemacht (siehe Abschnitt 4.2.1). Diese restore -Aktion kann natürlich auch genutzt werden, um eine Transaktion während der Deadlocksuche wieder rückgängig zu machen. Durch die Möglichkeit, Invarianten zu formulieren, ergibt sich jedoch noch eine Schwierigkeit. Wie in Abschnitt 4.3.2 erläutert wurde, werden alle am Ende einer Transaktion überprüften Invarianten zunächst aus den Transaktionsvariablen, mit denen sie assoziiert sind, ausgetragen und dann wieder in die TVars eingetragen, die sie bei der Überprüfung gelesen haben. Diese Änderung wird wieder nicht durch die restore -Aktion rückgängig gemacht und muss daher gesondert behandelt werden. 89 7 Implementierung des Debuggers für Transaktionen 7.2.1.1 Wiederherstellen der Invariantenlisten Um die überprüften Invarianten aus allen Transaktionsvariablen auszutragen, enthält jede Invariante eine IO-Aktion, die genau dies tut. Eine Möglichkeit, diesen Vorgang rückgängig zu machen, ist das Austragen jeder einzelnen Invariante rückgängig zu machen. Ezienter ist es jedoch, alle Invariantenlisten, die am Ende der Transaktion verändert werden, zu identizieren und deren alten Inhalt zurückzuschreiben. Dazu erhält der Konstruktor Invariant als weiteres Argument alle Invariantenlisten, in denen die Invariante eingetragen ist und aus denen sie dann am Schlusss ja auch wieder ausgetragen wird. Nun müssen noch die Invariantenlisten identiziert werden, die geändert werden, wenn die überprüften Invarianten wieder eingetragen werden. Die Aktion zum Eintragen der Invarianten in die Invariantenlisten wird während der abschlieÿenden Invariantenprüfung im Feld addInvars des Transaktionszustands ge- sammelt. Die Invariantenlisten, die durch diese Aktion geändert werden, können auf ähnliche Weise gesammelt werden. Der Transaktionszustand erhält daher ein neues Feld alteredInvarMVars, in das all die Invariantenlisten der Transaktionsvariablen, die während der abschlieÿenden Invariantenprüfung gelesen wurden, eingetragen werden. Listing 7.12 zeigt dies. checkInvar :: Invariant -> StmState -> P . IO ( STMResult ()) checkInvar ( Invariant id _ _ action ) stmState@TST {...} = do res <- action stmState { readTVars = []} case res of Success newState _ -> do let removeInvarAction = sequence_ ( map (( removeInvariant id ). snd ) ( readTVars newState )) newInvar = Invariant id ( map snd ( readTVars newState )) removeInvarAction action return ( Success ( newState {... , alteredInvarMVars = ( map snd ( readTVars newState )) ++ ( alteredInvarMVars newState ) }) ()) _ -> return res Listing 7.12: Identizierung der veränderten Invariantenlisten Kurz vor der abschlieÿenden Invariantenprüfung sind alle Invarianten, die geprüft werden, bekannt. Da die Invarianten nun auch die Listen enthalten, in denen sie enthalten sind, lässt sich vor der Prüfung das Feld alteredInvarMVars mit den Listen aus allen Invarianten initialisieren. Das heiÿt, nach der abschlieÿenden Invariantenprüfung enthält dieses Feld nun alle Invariantenlisten, aus denen Invarianten entfernt und in die Invarianten geschrieben werden. Daraus lässt sich eine IO-Aktion erstellen, die den alten Inhalt der Invariantenlisten wieder herstellt. Wie diese erzeugt und benutzt wird, folgt im nächsten Abschnitt. 90 7.2 Erweiterung des Steppers 7.2.1.2 Eine Transaktion als ConcAction Nun können Transaktionen in den IO-Baum integriert werden. Zusätzlich zu den bereits im vorigen Abschnitt erläuterten Änderungen muss dazu nur das Ausführen atomically (Listing 7.13) angepasst werden. Demnach ist ConcAction, wie sie bereits in Abschnitt 5.3.2.2 vorgestellt von Transaktionen durch eine Transaktion eine wurde. Deren erstes Argument ist die eigentliche nebenläuge Aktion, hier also die Transaktion. Ausführung und Invariantenprüfung nden wie bei der LightweightBibliothek statt. Erst wenn nach der Invariantenprüfung das Ergebnis zurückgeliefert wird, sind Änderungen nötig. atomically :: STM a -> IO a atomically stmAction = ConcAction ( do -- hier findet die Transaktion und die abschliessende -- Invariantenpruefung statt mit den angesprochenen -- Aenderungen case stmResult ' of Exception newState e -> ( Control . Exception . throw e ) Retry newState -> return Nothing Success newState res -> do let alteredMVars = nub ( alteredInvarMVars newState ) oldContent <- mapM C . readMVar alteredMVars let restoreInvars = zipWithM_ C . swapMVar alteredMVars oldContent commit newState removeInvars newState addInvars newState return ( Just ( res , ( restoreInvars >> restore newState )))) id Listing 7.13: Integration von Transaktionen in den IO-Baum Ist während der Transaktion eine Exception aufgetreten, so wird diese wieder geworfen. Wie in Abschnitt 5.3.6 erläutert, wird diese in der Funktion checkThread wieder abgefangen, um den Ausführungspfad zu dieser Exception zu ermitteln. Ist das Ergebnis Retry, so würde ein echter Thread suspendieren. Auch bei Transak- tionen soll der Stepper natürlich nicht blockieren. Daher wird, wie auch bei anderen ConcActions, das Suspendieren durch das Zurückgeben von Nothing signalisiert. Interessant wird es, wenn die Transaktion erfolgreich durchgeführt wurde. In diesem Fall werden zunächst die Invariantenlisten, die geändert werden, ausgelesen und eine restoreInvars erstellt, die diese Invariantenlisten wiederherstellt. Dann werden die Änderungen an den Werten der TVars und den Invariantenlisten tatsächlich durchgeführt. Durch das Zurückgeben von Just wird signalisiert, dass die Aktion Aktion 91 7 Implementierung des Debuggers für Transaktionen erfolgreich durchgeführt wurde. Mit zurückgegeben wird ein Paar, das zum einen das Ergebnis der Transaktion enthält, zum anderen eine Aktion, die alle in dieser Transaktion durchgeführten Änderungen rückgängig macht. Das zweite Argument der ConcAction ist eine Funktion, die den Wert im zweiten Teil des Ergebnispaares nimmt und eine Aktion liefert, die die nebenläuge Aktion wieder rückgängig macht. Hier reicht dafür die Identitätsfunktion id, da das Ergebnispaar bereits die vollständige Aktion enthält. Durch die Funktionsweise des CHS ergeben sich Vereinfachungen, die sich positiv auf die Performance der Suche auswirken. Da die Suche von nur einem Thread durchgeführt wird, der eine Liste von virtuellen Threads verwaltet, kann es nicht zu einer inkonsistenten Sicht auf die Transaktionsvariablen kommen. Wie in Listing 7.13 zu sehen, wird keine Konsistenzprüfung durchgeführt und auch kein globales Lock verwendet. Auÿerdem wird kein Thread tatsächlich suspendiert. Die Aktionen wait zum Eintragen des Threads in die während der Transaktion gelesenen Transaktionsvaria- notify, die wartende Threads benachrichtigt, werden nicht mehr benötigt. die Konsistenzprüfung oder die Aktionen wait und notify benötigten Teile blen und Die für in der Implementierung der einzelnen STM-Aktionen können daher einfach entfernt werden. 7.2.2 Partial Order Reduction Wie in Abschnitt 5.3.4 erläutert, nutzt der CHS Partial Order Reduction, um die Anzahl der zu durchsuchenden Zustände zu verringern. Dazu erhielt jede ConcAction ein zusätzliches Argument, das die beteiligte Kommunikationsabstraktion eindeutig identizierte und dadurch ermöglichte zu entscheiden, ob je zwei Aktionen voneinander abhängen. Dies war bisher recht leicht möglich, da immer nur eine einzige Kommunikationsabstraktion an einer nebenläugen Aktion beteiligt war, die schon vor der Ausführung der Aktion bekannt war. Bei Transaktionen ist dies jedoch nicht der Fall. In einer Transaktion können natürlich mehr als nur eine Transaktionsvariable eine Rolle spielen. Auÿerdem ist es unmöglich, diese vor der Ausführung einer Transaktion zu bestimmen. Glücklicherweise lässt sich hier eine der Haupteigenschaften von Transaktionen nutzen. Transaktionen lassen sich fast komplett ausführen, ohne dass tatsächlich Änderungen durchgeführt werden. Nach der Ausführung können dann auch die beteiligten Transaktionsvariablen bestimmt werden. Um dies zu nutzen, wird ein neuer Konstruktor Uneval für den Datentyp IO des CHS eingeführt (Listing 7.14). Ein Uneval Knoten im IO-Baum bedeutet, dass hier eine Aktion zunächst voraus- gewertet werden muss, bevor diese weiter verwendet werden kann. Dazu enthält der Konstruktor eine IO-Aktion, die einen neuen Knoten des IO-Baums liefert. 92 7.2 Erweiterung des Steppers data IO a = ... , Uneval ( P . IO ( IO a )) Listing 7.14: Konstruktor Uneval Transaktionen sind bisher das einzige Anwendungsgebiet für diesen neuen Knoten. atomically Uneval-Knoten. Die Dazu muss die im vorigen Abschnitt vorgestellte Implementierung von angepasst werden. Wie in Listing 7.15 ist atomically nun ein dazugehörende Aktion führt die Transaktion bis auf die tatsächlichen Änderungen aus und gibt je nach Transaktionsergebnis einen ConcAction-Knoten zurück. Im Fall ConcAction nur die einer erfolgreichen Transaktionsausführung führt die erzeugte tatsächlichen Änderungen aus, eine doppelte Ausführung der Transaktion ist daher nicht nötig. atomically stmAction = Uneval ( do -- Ausfuehrung der Transaktion und der -- Invariantenpruefung case stmResult ' of Exception newState e -> return ( ConcAction ( touchedTVars newState ) ( Control . Exception . throw e ) id ) Retry newState -> return ( ConcAction ( touchedTVars newState ) ( return Nothing ) id ) Success newState res -> do return ( ConcAction ( touchedTVars newState ) ( do -- aenderungen ausfuehren return ( Just ( res , ( restoreInvars >> restores newState )))) id )) Listing 7.15: Eine Transaktion als Jede der erzeugten Uneval-Knoten ConcActions enthält nun eine Liste der an der Transaktion beteitouchedTVars die Ver- ligten Transaktionsvariablen. Dazu berechnet die Funktion einigung aus den geschriebenen und gelesenen Transaktionsvariablen. Die Funktion dependentOn aus Abschnitt 5.3.4, die alle Threads identiziert, die von dem aktuell getesteten abhängen, muss nun auch so geändert werden, dass sie überprüft, ob die Listen der an einer ConcAction beteiligten Kommunikationsabstraktionen mindes- tens ein gleiches Element enthalten. Nun muss noch geklärt werden, an welcher Stelle und wie ein eine ConcAction Uneval-Knoten in verwandelt wird. Dies muss geschehen, bevor die Abhängigkeits- analyse durchgeführt wird. Und tatsächlich existiert bereits eine Aktion evalThread 93 7 Implementierung des Debuggers für Transaktionen (Abschnitt 5.3.6.1), die zum Zweck des Exceptionhandling eine Vorauswertung vornimmt, bevor irgendetwas mit dem IO-Baum geschieht. Diese Aktion kann nun zusätzlich die Aktionen der Uneval-Knoten ausführen und sie durch das Ergebnis er- setzen. Doch damit ist es leider noch nicht getan. Denn wird bei der Suche eine Transaktion ausgeführt und werden dadurch auch Transaktionsvariablen verändert, so würden sich möglicherweise die Transaktionen anderer Threads ändern. Dies ist jedoch nicht mehr möglich, wenn, wie gerade beschrieben, jede Transaktion nur noch durch ihre ConcAction Ergebnisaktion repräsentiert als vorliegt. Es wird also beides benötigt, die Ergebnisaktion zusammen mit den dann bekannten, beteiligten Transaktionsvariablen, aber auch die gesamte Transaktion, die nach der Ausführung einer anderen Transaktion erneut ausgewertet werden kann. Ein neuer Knoten Eval macht dies möglich. Wie in Listing 7.16 zu sehen, enthält dieser Knoten sowohl ein Argument vom Typ einer Transaktion in Form einer dem Uneval ConcAction, IO a, hier immer die Abschlussaktion als auch die IO-Aktion, die schon aus Knoten bekannt ist. data IO a = ... , Eval ( IO a ) ( P . IO ( IO a )) Listing 7.16: Konstruktor Ein Eval Uneval-Knoten wird von evalThread also nicht durch das Ergebnis der enthalteEval-Knoten, der sowohl das Ergebnis als nen IO-Aktion ersetzt, sondern durch einen auch die IO-Aktion enthält. Zur Identizierung von voneinander abhängigen Threads und auch beim Testen eines Threads durch gument des Eval-Knotens checkThread kann dann das erste Ar- verwendet werden. Nachdem eine Transaktion ausgeführt wurde, kann das zweite Argument von evalThread verwendet werden, um die ver- änderten Transaktionen auszuwerten. Listing 7.17 zeigt, wie evalThread mit den beiden neuen Knotentypen umgeht. evalThread ' ( tId , Uneval pio ) = do io <- pio return ( tId , Eval io pio ) evalThread ' ( tId , Eval _ pio ) = do io <- pio return ( tId , Eval io pio ) Listing 7.17: Auswertung von Uneval- und Eval-Knoten Eine Transaktion verändert ihr Verhalten nur, wenn die Ausführung einer anderen Transaktion Transaktionsvariablen verändert, die an ihr beteiligt sind. Daher ist es nicht notwendig, nach der Ausführung der nebenläugen Aktion eines Threads 94 7.2 Erweiterung des Steppers alle anderen durch evalThread neu auszuwerten. Es reicht, dies mit den von dieser Aktion abhängigen zu tun. Durch die Reduzierung des Suchraums bei Programmen mit Transaktionen, wird die Suche deutlich beschleunigt. So wird, bei der STM-Version der dinierenden Philosopen mit Deadlock aus Listing 7.11 mit fünf Philosophen, die Anzahl der durchsuchten Zustände bis Tiefe 10 von 13366 auf 6465 reduziert. Erhöht man die Anzahl der Philosophen, so verbessert sich dieses Verhältnis noch weiter. 7.2.3 Integration in den CHD Wie bereits im Abschnitt 5.3.5 erläutert, ist der CHS in den CHD integriert. Dazu enthalten die Kommunikationsabstraktionen des CHS zusätzlich die entsprechende Kommunikationsabstraktion des CHD. Auf diese Weise lässt sich die Suche und die Ausführung im CHD auf verschiedenen Kommunikationsabstraktionen durchführen. Erst wenn eine Aktion im CHD komplett ausgeführt wurde, wird die Suche angehalten und die verschiedenen Kommunikationsabstraktionen in einen konsistenten Zustand gebracht. Daher enthalten auch Transaktionsvariablen im CHS zusätzlich die im CHD verwendeten TVars. Listing 7.18 zeigt die neue Denition der Transak- tionsvariablen. data TVar a = TVar ( C . MVar a ) ID ( C . MVar [ Invariant ]) ( CD . TVar a) Listing 7.18: Datentyp Damit von der Aktion stepThread TVar im CHS der IO-Baum so interpretiert werden kann, dass die nebenläugen Aktionen im CHD dargestellt werden und nach der Ausführung einer Aktion die Suche ausgehend von dem neuen Zustand wieder gestartet werden kann, besitzt jede ConcAction eine zusätzliche Aktion. Auch Transaktionen werden im IO-Baum als nach Auswertung des Uneval-Knotens. ConcAction dargestellt, wenn auch erst Daher kann die CHD-Aktion genau wie bei anderen nebenläugen Aktionen angegeben werden. Ein Unterschied besteht jedoch darin, dass eine Transaktion aus mehreren STM-Aktionen zusammengesetzt wird. Um sowohl eine Transaktion für den CHD als auch für den CHS aufbauen zu können, muss der Datentyp STM für den CHS angepasst werden. Jede STM-Aktion enthält nun auch die dazugehörende STM-Aktion des CHD. Dadurch ist es möglich, eine Transaktion für den CHD sichtbar auszuführen. Wurde eine solche Transaktion allerdings komplett ausgeführt, müssen auch die den dabei veränderten Transaktionsvariablen des CHD entsprechenden Transaktionsvariablen des CHS verändert werden. Der Ansatz, die gesamte Transaktion für den CHS einfach noch einmal auszuführen, funktioniert leider nicht, denn innerhalb einer Transaktion können auch neue Transaktionsvariablen erzeugt werden. Würden nun Transaktionen einmal für den CHD und einmal für den CHS ausgeführt, so würden diese neuen Transakti- 95 7 Implementierung des Debuggers für Transaktionen onsvariablen auch zweimal erzeugt und sich nachträglich auch nicht wieder zu einer kombinieren lassen. Wie aber lassen sich die Transaktionsvariablen des CHD und des CHS nach der Ausführung der CHD-Transaktion in einen konsistenten Zustand bringen? Um dies zu erreichen, muss die Kenntnis über die genaue Implementierung der CHD-Transaktionen ausgenutzt werden. Am Beispiel von writeTVar wird in Listing 7.19 demonstriert, wie dies funktioniert. writeTVarLabelLine :: CodePosition -> String -> TVar a -> a -> STM () writeTVarLabelLine pos label ( TVar chstvar id cdtvar invars ) v = STM -- hier ist die STM - Aktion des CHS ( CD . writeTVarLabelLine pos label cdtvar v > >= CD . STM (\ state -> do let co = CD . commit state return ( CD . Success state { CD . commit = ( do co C . takeMVar chstvar C . putMVar chstvar v )} ()))) Listing 7.19: Einschleusen der CHS-Aktion in die Aktion des CHD Das zweite Argument der dargestellten STM-Aktion ist die Aktion des CHD. Allerdings ist dies nicht einfach direkt die original CHD-Aktion, sondern zusätzlich wird der Transaktionszustand so manipuliert, dass beim Abschluss der Transaktion durch die commit-Aktion gleich noch die Transaktionsvariablen des CHS mit verändert werden. Auch wenn die Idee, die commit-Aktion zu manipulieren, beibehalten werden kann, ist dies nicht so möglich, wie es gerade vorgestellt wurde. Probleme bereitet dabei die Implementierung des Lesens von während der Transaktion bereits geschriebener Transaktionsvariablen. Dafür wird, wie in Abschnitt 4.2.1 beschrieben, zunächst die commit-Aktion ausgeführt, die Transaktionsvariable gelesen und die Änderun- gen durch Ausführen der restore-Aktion wieder rückgängig gemacht. Das Problem ist nun, dass während der Ausführung der Transaktion im CHD die Suche im CHS weiterläuft. Wird dabei dann von einem anderen Thread der Wert in einer Transaktionsvariable des CHS verändert, auch wenn dies wieder rückgängig gemacht wird, kann das Ergebnis der Suche verändert werden. Also darf bei Ausführung der commit-Aktion die Transaktionsvariable des CHS nicht direkt verändert werden. Stattdessen wird die Aktion, die die Transaktionsvariablen verändert, durch die commit-Aktion in eine MVar geschrieben und kann später gesonMVar muss in jeder STM-Aktion bekannt sein, dadurch dert ausgeführt werden. Diese ergibt sich die in Listing 7.20 dargestellte Denition des Datentyps STM. Die Denition von ting 7.21 wird die 96 writeTVar kann nun angepasst werden. In commit-Aktion so manipuliert, dass die zur der Denition in LisÄnderung der Trans- 7.2 Erweiterung des Steppers data STM a = STM ( StmState -> P . IO ( STMResult a )) ( MVar ( P . IO ()) -> CD . STM a ) Listing 7.20: Datentyp STM im CHS aktionsvariable des CHS nötige Aktion an eine schon vorhandene IO-Aktion in der übergebenen MVar angehängt wird. Um dies bei einem Aufruf von rückgängig zu machen, wird die MVar einfach mit return () restore wieder gefüllt. writeTVarLabelLine pos label ( TVar chstvar id cdtvar invars ) v = STM -- hier ist die STM - Aktion des CHS (\ chsMVar -> CD . writeTVarLabelLine pos label cdtvar v > >= CD . STM (\ state -> do let co = CD . commit state rest = CD . restore state chsAction = ( do C . takeMVar chstvar C . putMVar chstvar v ) return ( CD . Success state { CD . commit =( do co oldAction <- C . takeMVar chsMVar C . putMVar chsMVar ( action >> chsAction )) , CD . restore = ( do C . takeMVar chsMVar C . putMVar chsMVar ( return ()) rest )} ()))) Listing 7.21: Sammeln der Aktionen für den CHS in einer MVar Der groÿe Vorteil an dieser Konstruktion wird deutlich, wenn man den STM-Kom- orElse in Listing 7.22 betrachtet. Hier muss nichts weiter getan werden, als den orElse-Kombinator des CHD zu verwenden und die MVar in die einzelnen STM-Aktionen weiterzureichen. Da im Fall eines Aufrufs von retry in der ersten STM-Aktion von orElse die dort durchgeführten Änderungen an der commit-Aktion binator verworfen werden, werden auch die Aktionen, die die Aktionen zur Änderung der Transaktionsvariablen des CHS in die MVar schreiben, verworfen. orElseLine :: CodePosition -> STM a -> STM a -> STM a orElseLine pos ( STM stm1 cdstm1 ) ( STM stm2 cdstm2 ) = STM -- hier befindet sich die Definition fuer den CHS (\ chsMVar -> CD . orElseLine pos ( cdstm1 chsMVar ) ( cdstm2 chsMVar )) Listing 7.22: CHD-STM-Aktion von orElse in der Denition des CHS Tatsächlich können fast alle STM-Aktionen auf diese Weise direkt verwendet werden. Eine Ausnahme ist newTVar, hier muss nicht nur die TVar für den CHD erzeugt und 97 7 Implementierung des Debuggers für Transaktionen zurückgegeben werden, sondern die gesamte Datenstruktur aus Listing 7.18. Damit stepThread eine Transaktion nun auch ausführen kann, erhält die ConcAction, die die Transaktion repräsentiert, die Funktion aus Listing 7.23 als Argument. Diese Funktion erhält die üblichen Argumente zur Unterscheidung, ob die Aktion inner- unsafePerformIO ausgeführt wird, und zum Aufbau eines neuen IO-Baums erfolgreichem Abschluss. Dann wird eine spezielle Version von atomicaly des halb von nach CHD aufgerufen, die nur im Fall einer erfolgreichen Transaktionsausführung noch durch das globalLock geschützt die ihr übergebene Aktion ausführt. In dieser Akti- on ndet die Kommunikation mit dem CHS statt. Auÿerdem wird hier nun auch die durch die commit-Aktion in die MVar geschriebene Aktion zur Änderung der Trans- aktionsvariablen des CHS ausgeführt. (\ unsafe treeFct -> do chsMVar <- C . newMVar ( return ()) res <- CD . atomicallyCHS (\ result -> do if ( not unsafe ) then do C . putMVar breakMVar () C . takeMVar finishedBreakMVar else return () action <- C . readMVar chsMVar action if ( not unsafe ) then C . putMVar treeMVar ( treeFct result ) else return ()) pos ( cdSTM chsMVar ) return res ) Listing 7.23: Aktion zur Ausführung einer Transaktion im CHD Bis auf eine Ausnahme ist die Integration des CHS mit Transaktionen in den CHD nun funktionsfähig. Das Problem sind einmal mehr die Invarianten. Sowohl die Datenstruktur für Transaktionsvariablen des CHS als auch die darin enthaltene Datenstruktur des CHD besitzen eine eigene Invariantenliste. Wird eine Transaktion im CHD ausgeführt, so werden auch nur die Listen der Transaktionsvariablen des CHD angepasst. Da der nale Invariantentest im CHD, der für die Änderung dieser Listen verantwortlich ist, von auÿen nicht so leicht manipuliert werden kann wie die einzelnen STM-Aktionen, ist es ohne gröÿere Änderungen am Quelltext der Transaktionsbibliothek des CHD nicht möglich, die Änderungen an den Invariantenlisten des CHS auch über die Aktion in der MVar durchzuführen. Um solche Änderungen am Quelltext zu vermeiden, kann jedoch die Invariantenprüfung für den CHS zusätzlich durchgeführt werden. Das Argument, das dieses Vorgehen für ganze Transaktionen verbat, gilt hier nicht, da bei einer erfolgreichen Invariantenprüfung keine Referenzen auf dabei erzeugte Transaktionsvariablen nach auÿen gelangen können. 98 7.2 Erweiterung des Steppers Um die Invariantenprüfung für den CHS durchzuführen,muss allerdings bekannt sein, welche Invarianten überhaupt geprüft werden sollen, also welche Transaktionsvariablen während der Transaktion geändert und welche Invarianten dabei neu erzeugt wurden. Glücklicherweise lässt sich diese Information genauso erhalten, wie das bei der Aktion zur Änderung der Transaktionsvariablen des CHS möglich ist. Die MVar, die bisher nur eine IO-Aktion enthielt, soll nun die Datenstruktur aus Listing 7.24 enthalten. Das Feld action nimmt die bisherige Aktion zur Änderung der Trans- aktionsvaribalen des CHS auf. Nun muss lediglich in writeTVar die commit-Aktion so manipuliert werden, dass sie die Invariantenliste der Transaktionsvariablen dem Feld chsTestInvars hinzufügt. In alwaysSucceeds und always commitchsNewInvars wird die Aktion so manipuliert, dass dies mit neuen Invarianten und dem Feld geschieht. data CHDInfo = CHDInfo { action :: P . IO () , chsNewInvars :: [ Invariant ] , chsTestInvars :: [ C . MVar [ Invariant ]]} Listing 7.24: Datenstruktur CHDInfo Die Aktion aus Listing 7.23 kann jetzt so angepasst werden, dass nach der Ausführung der Änderungen an den Transaktionsvariablen durch die bereits bekannte Aktion checkInvars die Invarianten getestet und vor allem die zur Änderung der In- variantenlisten benötigten Aktionen erzeugt werden können. Das eigentliche Testen der Invarianten ist dabei nebensächlich, da dies nur geschieht, wenn die Invarianten im CHD bereits erfolgreich getestet wurden. 99 7 Implementierung des Debuggers für Transaktionen 100 8 Zusammenfassung und Ausblick In dieser Arbeit wird gezeigt, wie die dem Concurrent Haskell Debugger zugrunde liegende Idee, durch eine veränderte Concurrent -Bibliothek Nachrichten an einen Debugger zu senden und die Aktionen grasch darzustellen, aufgegrien und auch für Transaktionen verwendet werden kann. Dazu werden in den Kapiteln 2 und 3 die Programmiersprache Haskell und eine ihrer Erweiterungen, Concurrent Haskell vorgestellt. In Kapitel 4 wird eine weitere Erweiterung von Haskell erläutert, die STM -Bibliothek. Diese ermöglicht die Verwendung von Transaktionen in nebenläugen Haskell-Programmen. Zusätzlich wird die bereits existierende Implementierung einer komplett in Haskell geschriebenen Version der STM -Bibliothek vorgestellt und erläutert, wie diese, um die Möglichkeit, Invarianten zu formulieren, erweitert werden kann. Verschiedene Debugger für Haskell werden kurz in Kapitel 5 beschrieben. Wie nebenläuge Haskell -Programme durch den Concurrent Haskell Debugger dargestellt werden und wie dies funktioniert, wird ausführlich erläutert. Es wird erklärt, wie der Concurrent Haskell Stepper den Debugger um eine Deadlocksuche erweitert. Auÿer- dem wird gezeigt, wie die bisher fehlende Unterstützung von unsafePerformIO und Exceptions ergänzt werden kann und dies genutzt wird, um nicht nur nach Deadlocks, sondern auch nach nicht abgefangenen Exceptions zu suchen. Kapitel 6 zeigt, wie Transaktionen im Concurrent Haskell Debugger dargestellt werden können. Und schlieÿlich wird in Kapitel 7 erläutert, wie die in Haskell geschriebene STM -Bibliothek genutzt wird, um die Darstellung von Transaktionen im Concurrent Haskell Debugger und die Nutzung des Concurrent Haskell Steppers zu er- möglichen. Zwar wurde der erweiterte Debugger noch nicht an realen Anwendungen getestet, doch zeigen die Tests an kleineren Beispielprogrammen, dass das Verhalten von Transaktionen auf verständliche und intuitive Weise dargestellt wird. Durch die zusätzliche Erweiterung des CHS ist es auch möglich, in Programmen mit Transaktionen nach Deadlocks zu suchen. Genauso wichtig ist, dass der CHS nun nicht nur Deadlocks, sondern zusätzlich auch nicht abgefangene Exceptions nden kann. Dies ermöglicht, nun zum Beispiel gezielt nach einer möglichen Verletzung von Invarianten zu suchen. Der erweiterte Concurrent Haskell Debugger könnte durch die nun verbesserte Abdeckung von nebenläugen Aktionen sowohl zu einer Verbesserung der Akzeptanz 101 8 Zusammenfassung und Ausblick von Haskell bei Entwicklern führen als auch den Einsatz von Transaktionen in Haskell vorantreiben. Zusätzlich könnte der erweiterte CHD nun auch in der Lehre zur Visualisierung der Funktionsweise von Transaktionen in Haskell eingesetzt werden. Dennoch bleibt noch viel Raum für Verbesserungen des CHD und auch der Darstellung von Transaktionen. Einige Ideen, wie die Entwicklung weitergehen könnte, sollen hier kurz vorgestellt werden. Zunächst einmal sind Verbesserungen am Debugger selbst möglich: So werden vom Debugger an die GUI sehr auf die Darstellung zugeschnittene Nachrichten gesendet. Um den Debugger um eine neue Darstellung der Aktionen, zum Beispiel in Form von Sequenzdiagrammen, zu erweitern, müsste man den Debugger so ändern, dass zusätzliche Nachrichten an die GUI gesendet werden, die auf die neue Darstellung zugeschnitten sind. Wünschenswert wäre, dass der Debugger, wie bei einem Model-View-System, nur Informationen über den Zustand des nebenläugen Programms verwaltet und die Darstellung der GUI überlässt. Bei der Deadlocksuche des CHS werden nebenläuge Aktionen wieder rückgängig gemacht. Dieses Prinzip könnte sich auch im CHD anwenden lassen, um dem Benutzer die Möglichkeit zu geben, einige Ausführungsschritte zurückzunehmen. Ein weiterer Verbesserungsvorschlag betrit die Darstellung des Inhalts von Kommunikationsabstraktionen. So ist es nicht immer möglich, den Inhalt einer Kommunikationsabstraktion durch ein statisches Label zu bestimmen. Auch die Verwendung der Funktion show zur dynamischen Darstellung des Inhalts ist problematisch, da sich dadurch das Auswertungsverhalten des Programms ändert. Möglich wäre jedoch, den Inhalt von Kommunikationsabstraktionen auf ähnliche Weise zu bestimmen, wie dies bei HOOD (siehe Abschnitt 5.1.1) geschieht. Auch die Darstellung von Transaktionen lässt sich noch weiter verbessern. So ist es bisher nicht direkt möglich zu bestimmen, welche Invarianten mit einer bestimmten Transaktionsvariable assoziiert sind. Erst durch die Invariantenprüfung vor dem Abschluss einer Transaktion wird deutlich, welche Invarianten mit den geänderten Transaktionsvariablen assoziiert sind. Es wäre wünschenswert, zu jeder Transaktionsvariablen eine Liste anzeigen zu können, die, zum Beispiel wieder durch ein Label, die dazugehörenden Invarianten beinhaltet. MVars anzeigen lassen, sondern auch darauf aufgebaute Abstraktionen, wie zum Beispiel der Datentyp Chan. Die Anzeige von Kommunikationsabstraktionen bei Transaktionen ist jedoch auf TVars Einer der Vorzüge des CHD ist, dass sich damit nicht nur beschränkt, auch wenn auf diese aufgebaute Abstraktionen existieren. Die Schwierigkeit besteht in der recht detaillierten Darstellung von Transaktionen, die sich oft direkt auf einzelne TVars bezieht. Ob und wie ein Kompromiss gefunden werden kann, der sowohl der Darstellung der Funktionsweise von Transaktionen als auch zusätzlichen Abstraktionen Rechnung trägt, ist eine interessante Frage. 102 Inhalt der CD Auf der beiliegenden CD bendet sich zum einen dieses Dokument als PDF, zum anderen der im Rahmen dieser Arbeit entstandene erweiterte Concurrent Haskell 1 Debugger als Cabal -Package . Die darin enthaltenen Quelltexte stimmen aus Dar- stellungsgründen in vielen Fällen nicht mit den in dieser Arbeit vorgestellten überein. 2 und das Um den CHD zu installieren, werden der ghc 6.6 3 ab Version Gtk2Hs -Paket 0.9.10.5 benötigt. Um den CHD mit Version 6.8 des ghc zu verwenden, sind aufgrund von Änderungen an der Paketstruktur einige wenige Anpassungen an der Cabal -Datei nötig. Auÿerdem sind noch eine Reihe von Beispielprogrammen auf der CD vorhanden. 1 The Haskell Cabal http://www.haskell.org/cabal/ The Glasgow Haskell Compiler http://www.haskell.org/ghc/ 3 Gtk2Hs http://www.haskell.org/gtk2hs/ 2 103 Inhalt der CD 104 Listings 2.1 Fibonacci . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.2 Anwendungsbeispiel für 2.3 succList 2.4 Lambda-Ausdruck in Haskell 2.5 succList 2.6 Fibonacci mit Pattern Matching 2.7 Pattern Matching mit . . . . . . . . . . . . . . . . 6 2.8 Eigener Datentyp . . . . . . . . . . . . . . . . 7 2.9 Typannotationen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 ohne map map 4 . . . . . . . . . . . . . . . . . . . . . . . 4 . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 . . . . . . . . . . . . . . . . . . . . . . 5 mit partieller Applikation . . . . . . . . . . . . . . . . . . . 5 case-Ausdrücken IntList . . . . . . . . 2.10 Beispiele für Polymorphie 2.11 Typklasse Eq . . . . . . . . . . . . . . . . . . . . 6 . . . . . . . . . . . . . . . . . . . . . . . . 7 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 2.12 Instantiierung der Klasse 2.13 Einschränkung von notEq Eq durch InList . . . . . . . . . . . . . . . Eq 8 . . . . . . . . . . 8 2.14 Länge einer Liste groÿer Fibonacci-Zahlen . . . . . . . . . . . . . . . 9 2.15 Die unendliche Liste der Primzahlen . . . . . . . . . . . . . . . . . . 9 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 2.16 Der Datentyp IO auf Typen der Klasse 2.17 Funktionen zur Ein- und Ausgabe von Zeichen 2.18 IO-Kombinatoren und return-Aktion . . . . . . . . . . . . . 10 . . . . . . . . . . . . . . . . . 10 . . . . . . . . . . . . . . . . . . . . . . . 11 . . . . . . . . . . . . . . . . . . . . . . . . . . 11 2.21 Funktionen zum Exceptionhandling in Haskell . . . . . . . . . . . . . 12 2.19 Ein einfaches IO-Programm 2.20 Beispiel in do-Notation IORef . . . . . . . . . Verwendung von unsafePerformIO Die Klasse Monad . . . . . . . . . . 2.22 Interface von 2.23 . . . . . . . . . . . . . . . . . . . 12 . . . . . . . . . . . . . . . . . . . 13 . . . . . . . . . . . . . . . . . . . 13 2.25 Struktur eines Haskell-Moduls . . . . . . . . . . . . . . . . . . . . . . 14 2.24 forkIO . . . . MVars 3.1 Typ von 3.2 Operationen auf . . . . . . . . . . . . . . . . . . . . . . . . . . 16 . . . . . . . . . . . . . . . . . . . . . . . . . . 17 3.3 Dinierende Philosophen in Concurrent Haskell . . . . . . . . . . . . . 19 3.4 Dinierende Philosophen mit globalem Lock . . . . . . . . . . . . . . . 20 4.1 Typ von 4.2 Operationen auf 4.3 Beispiel für eine zusammengesetzte Transaktion 4.4 Demonstration von erhalten atomically . TVars . . . . . . . . . . . . . . . . . . . . . . . . . . 22 . . . . . . . . . . . . . . . . . . . . . . . . . . 22 retry swapTVar . . . . . . 22 um exklusiven Zugri auf eine Datei zu . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23 4.5 Exklusiver Zugri auf zwei Dateien . . . . . . . . . . . . . . . . . . . 24 4.6 Dinierende Philosophen mit Transaktionen . . . . . . . . . . . . . . . 24 105 Listings orElse . 4.7 Typ von . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24 4.8 Alternativer Zugri auf zwei Dateien . . . . . . . . . . . . . . . . . . 25 4.9 Exceptionhandling in Transaktionen . . . . . . . . . . . . . . . . . . 25 4.10 Erzeugung von Invarianten in Transaktionen . . . . . . . . . . . . . . 26 LimitedTVar 4.11 Invarianten-Beispiel: . . . . . . . . . . . . . . . . . . . 27 4.12 Die STM Monade . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28 TVars TVars . . 4.13 Schreiben von 4.14 Lesen von . . . . . . . . . . . . . . . . . . . . . . . . . . . 28 . . . . . . . . . . . . . . . . . . . . . . . . . . . 29 TVar . . . . . . . . . . . . . . . . . . . . . . . . 29 . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30 4.15 Erzeugen einer neuen 4.16 Konsistenzprüfung 4.17 Implementierung von atomically . . . retry orElse . . . . . catchSTM . . . . alwaysSucceeds always . . . . . . . . . . . . . . . . . . . . . . 31 4.18 Änderungen zur Einführung von . . . . . . . . . . . . . . . . . 32 4.19 Implementierung von . . . . . . . . . . . . . . . . . 33 . . . . . . . . . . . . . . . . . 34 4.20 Implementierung von 4.21 Implementierung von 4.22 Implementierung von 4.23 Geänderte Typen für Invarianten . . . . . . . . . . . . . . . . . 35 . . . . . . . . . . . . . . . . . 35 . . . . . . . . . . . . . . . . . . . . 36 4.24 Testen einer Invariante am Ende einer Transaktion . . . . . . . . . . 37 . . . . . . . . . . . . . . . . . 38 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41 putMVar 4.25 Testen der Invarianten in observe atomically 5.1 Typ von 5.2 Erster Ansatz für . . . . . . . . . . . . . . . . . . . . . . . . 50 5.3 Denition des Nachrichtenkanals und Starten des Debuggers . . . . . 51 5.4 Neue Datenstruktur für . . . . . . . . . . . . . . 5.5 Denition von einer neuen 5.6 Benachrichtigung des Debuggers beim Entfernen einer 5.7 Unterbrechen von Threads durch Suspendieren 5.8 Redenierter Datentyp 5.9 5.10 5.11 5.12 5.13 5.14 5.15 5.16 5.17 5.18 5.19 5.20 5.21 5.22 MVars . . . . . . . . newEmptyMVar mit Zuweisung MVarNo MVar . . . 51 . . 52 . . 52 . . . . . . . . . . . . 53 IO a . . . . . . . . . . . . . . . . . . . . . . . Neudenition von newEmptyMVar für den CHS . . . . . . . . . . . . . Neudenition von takeMVar für den CHS . . . . . . . . . . . . . . . Neudenition von forkIO und killThread für den CHS . . . . . . . Datentyp ActionType . . . . . . . . . . . . . . . . . . . . . . . . . . Denition von putChar und getChar für den CHS . . . . . . . . . . Datentyp Thread für die Deadlocksuche . . . . . . . . . . . . . . . . Typ der Funktion checkThread . . . . . . . . . . . . . . . . . . . . . Typ von checkThreads . . . . . . . . . . . . . . . . . . . . . . . . . . Die Funktion checkThreads nach dem Testen aller Threads einer Ebene Testen eines Threads mit checkThreads . . . . . . . . . . . . . . . . checkThreads mit Reduktion des Suchraums . . . . . . . . . . . . . Funktion stepThread für SeqActions . . . . . . . . . . . . . . . . . . Datentyp MVar a im CHS . . . . . . . . . . . . . . . . . . . . . . . . Denition von putMVar für den CHS . . . . . . . . . . . . . . . . . . 56 57 57 57 58 59 59 59 60 60 61 62 63 64 64 5.23 Beispiel für das Abfangen einer Exception bei einer IO-Aktion . . . . 66 5.24 Beispiel für eine Exception bei der Auswertung des IO-Baums . . . . 66 106 Listings 7.5 evalThread . . . . . . . . . . . . . . . . . . . . . . . . Denition von catch und Catch . . . . . . . . . . . . . . . . . . . . . Denition von checkThread zur Behandlung von Catch . . . . . . . Denition von executeUnsafely und unsafePerformIO für den CHS Problematischer Einsatz von unsafePerformIO . . . . . . . . . . . . Nachrichten an den Debugger in readTVar . . . . . . . . . . . . . . . Implementierung von newTVar für den CHD . . . . . . . . . . . . . . Implementierung von orElse für den CHD . . . . . . . . . . . . . . . Ermitteln von zu entfernenden und wieder herzustellenden lokalen TVars Implementierung von always für den CHD . . . . . . . . . . . . . . . 7.6 Nachrichten an den Debugger nach erfolgreicher Transaktionsausführung 85 7.7 Nachrichten an den Debugger beim Neustart durch . . . . . . 86 7.8 Nachrichten an den Debugger beim Propagieren einer Exception . . . 87 7.9 Nachrichten vom Debugger an den Thread . . . . . . . . . . . . . . . 88 5.25 Denition von 66 5.26 67 5.27 5.28 5.29 7.1 7.2 7.3 7.4 7.10 Erfragen einer neuen TVarNo retry 67 68 69 78 79 81 82 84 beim Debugger . . . . . . . . . . . . . . 88 7.11 STM-Version der dinierenden Philosophen mit Deadlock . . . . . . . 89 7.12 Identizierung der veränderten Invariantenlisten . . . . . . . . . . . . 90 7.13 Integration von Transaktionen in den IO-Baum . . . . . . . . . . . . 91 . . . . . . . . . . . . . . . . . . . . . . . . . . . 93 7.14 Konstruktor Uneval Uneval-Knoten . . . . Eval . . . . . . . . . . . . . . . Auswertung von Uneval- und Eval-Knoten Datentyp TVar im CHS . . . . . . . . . . . 7.15 Eine Transaktion als . . . . . . . . . . . . . . 93 7.16 Konstruktor . . . . . . . . . . . . . . 94 7.17 . . . . . . . . . . . . . . 94 . . . . . . . . . . . . . . 95 7.19 Einschleusen der CHS-Aktion in die Aktion des CHD . . . . . . . . . 96 7.18 7.20 Datentyp STM im CHS . . . . . . . . . . . . . . . . . . . . . . . . . . 7.21 Sammeln der Aktionen für den CHS in einer 7.22 CHD-STM-Aktion von orElse MVar . 97 in der Denition des CHS . . . . . . . 97 7.23 Aktion zur Ausführung einer Transaktion im CHD 7.24 Datenstruktur CHDInfo . 97 . . . . . . . . . . . . . . . . . . . . 98 . . . . . . . . . . . . . . . . . . . . . . . . . 99 107 Listings 108 Abbildungsverzeichnis 3.1 Programmablauf mit GUI . . . . . . . . . . . . . . . . . . . . . . . . 15 3.2 Programmablauf mit GUI und Threads . . . . . . . . . . . . . . . . . 16 3.3 Das Problem der dinierenden Philosophen . . . . . . . . . . . . . . . 18 5.1 Screenshot des Concurrent Haskell Debuggers . . . . . . . . . . . . . 46 5.2 Darstellung eines Channels im CHD 47 5.3 Darstellung eines Labels als Inhalt einer . . . . . . . . . . . . . 48 5.4 Screenshot der Quelltextanzeige . . . . . . . . . . . . . . . . . . . . . 50 5.5 Kommunikation zwischen Threads, Debugger und GUI . . . . . . . . 54 5.6 Darstellung der Ausführungsreihenfolgen als Baum 55 6.1 Screenshot des Hauptfensters des CHD: STM-Version der dinierenden Philosophen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72 6.2 Screenshot des Transaktionsfensters des CHD 74 . . . . . . . . . . . . . . . . . . MVar . . . . . . . . . . . . . . . . . . . . . . . 109 Abbildungsverzeichnis 110 Literaturverzeichnis [1] Böttcher, Thomas: Entwicklung eines Debuggers für Concurrent Haskell. Diplomarbeit, Rheinisch-Westfälische Technische Hochschule Aachen, 2001. [2] Böttcher, Thomas und Frank Huch: A Debugger for Concurrent Haskell. In: In Proceedings of the 14th International Workshop on the Implementation of Functional Languages, Madrid, Spain, September 2002. [3] Buddha. http://www.cs.mu.oz.au/~bjpop/buddha/. Online; 23.04.2008. [4] Christiansen, Jan und Frank Huch: Searching for deadlocks while debugging concurrent haskell programs. In: ICFP '04: Proceedings of the ninth ACM SIGPLAN international conference on Functional programming, Seiten 2839, New York, NY, USA, 2004. ACM. [5] Church, Alonzo: The Calculi of Lambda Conversion. Princeton University Press, 1941. [6] The Glasgow Haskell Compiler. http://haskell.org/ghc. Online; 28.04.2008. [7] Gill, Andy: Debugging Haskell by observing intermediate data structures. In: Proceedings of the 2000 Workshop on Haskell, Technical report of the University of Nottingham, 2000. [8] Harris, Tim, Simon Marlow, Simon Peyton Jones und Maurice Herlihy: Composable Memory Transactions. In: PPoPP '05: Proceedings of the tenth ACM SIGPLAN symposium on Principles and practice of parallel programming, Seiten 4860, New York, NY, USA, 2005. ACM Press. [9] Harris, Tim und Simon Peyton Jones: Transactional memory with data invariants. In: First ACM SIGPLAN Workshop on Languages, Compilers and Hardware Support for Transactional Computing (TRANSACT'06), June 2006. [10] Haskell. [11] Haskell'. http://haskell.org. Online; 28.04.2008. http://hackage.haskell.org/trac/haskell-prime/wiki. Online; 19.04.2008. [12] Hat. http://www.haskell.org/hat/. Online; 23.04.2008. 111 0 Literaturverzeichnis [13] Huch, Frank und Frank Kupke: A High-Level Implementation of Composable Memory Transactions in Concurrent Haskell. In: Butterfield, Andrew, Clemens Grelck und Frank Huch (Herausgeber): IFL, Band 4015 der Rei- he Lecture Notes in Computer Science, Seiten 124141. Springer, 2005. [14] Huch, Frank und Klaas Ole Kürtz: Funktionale Programmierung. www.kuertz.name/files/FunktionaleProgrammierung.pdf, http:// 2005. Vorlesung; Online; 19.04.2008. [15] Hudak, Paul, John Hughes, Simon Peyton Jones und Philip Wadler: A history of Haskell: being lazy with class. In: HOPL III: Proceedings of the third ACM SIGPLAN conference on History of programming languages, New York, NY, USA, 2007. ACM. [16] Peyton Jones, Simon (Herausgeber): Haskell 98 Language and Libraries: The Revised Report. Cambridge University Press, May 2003. [17] Peyton Jones, Simon, Andrew Gordon und Sigbjorn Finne: Concurrent Haskell. In: 23rd ACM Symposium on Principles of Programming Languages, Seiten 295308, St Petersburg Beach, Florida, January 1996. ACM. [18] Peyton Jones, Simon, Alstair Reid, Tony Hoare, Simon Marlow und Henderson Fergus: A Semantics for Imprecise Exceptions. In: SIGPLAN Conference on Programming Language Design and Implementation, Seiten 25 36, 1999. [19] Pope, Bernhard: Buddha: A declarative debugger for Haskell. Technischer Bericht, Dept. of Computer Science, University of Melbourne, 1998. Honours Thesis. [20] Schönfinkel, M.: Über die Bausteine der mathematischen Logik. Mathematische Annalen, 92(3 - 4):305316, 1924. [21] Sparud, Jan und Colin Runciman: Complete and Partial Redex Trails of Functional Computations. In: IFL '97: Selected Papers from the 9th Interna- tional Workshop on Implementation of Functional Languages, Seiten 160177, 1998. 112