Erweiterung des Concurrent Haskell Debuggers für

Werbung
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
Herunterladen