=head1 Ereignisgesteuerte Programmierung - die Realität =head2 Zusammenfassung Ereignisgesteuerte Programmierung ist hinreichend bekannt. Trotzdem wird sie relativ selten benutzt - in Modulen so gut wie garnicht. Ich möchte hier verschiedene Arten der Ereignissteuerung sauber von anderen Formen der Programmierung trennen, vorhandene Ereignismodule und ihre Unterschiede vorstellen. Vor allem sollen Möglichkeiten gezeigt werden, wie man als Modulautor - je nach gewünschtem Aufwand - verschiedene APIs anbieten kann. =head1 Ereignissteuerung - In aller Kürze Ereignissteuerung ist - für mich - kein sehr exakt festgelegter Begriff oder eine festgelegte Syntax. Daher will ich einige Beispiele für verschiedene Arten bzw. Grade der Ereignissteuerung, wie sie in der Praxis vorkommen, vorstellen. Üblicherweise verwendet man Ereignissteuerung immer dann, wenn man genügend Rechenzeit für die Verarbeitung vieler Aufgaben hat, jedoch durch externe Umstände (I/O-Geschwindigkeit, Netzwerk, User klickt nicht schnell genug :) gezwungen ist, auf Daten bzw. Ereignisse zu warten. Ereignissteuerung bedeutet dann, ein Programm derart zu strukturieren, daß es auf Ereignisse wartet und - möglichst schnell - reagiert. Die Haupt-Eingabe besteht sozusagen in einem Strom von Ereignissen (Event-Stream). Das Ziel der Ereignissteuerung ist es, die Gesamtdauer aller Operationen zu verringern und die Reaktionsgeschwindigkeit zu erhöhen. Unter Unix gibt es eine zentrale Funktion, mit der ein Programm auf Ereignisse warten kann: C unterscheiden, sowie kompliziertere Methoden wie realtime-Signale, die aber alle auf dasselbe hinauslaufen). C etwas low-level ist, gibt es Bibliotheken und Module, die diese Funktionalität in eine freundlichere Schnittstelle gießen - das nennt man dan "Event-Modell". Daraus folgt, daß das "Hauptprogramm" der Aufruf dieser Schleife/dieses Event-Modells ist, und alle Ereignisse in Unterfunktionen behandelt werden müssen. Daher lassen sich in einem Programm auch sehr schwer mehrere Event-Modelle miteinander mischen, da nur jeweils eines die Kontrolle über den Programmablauf haben kann. =head1 Formen der Ereignissteuerung =head2 Linearer Ablauf - "Nicht ereignisgesteuert" Wir alle kennen (hoffe ich) LWP, mit dem man sehr einfach Webseiten u.ä. abrufen kann: use LWP::Simple; print get "http://www.cpan.org/"; Ein heutiger Rechner kann das in Millisekunden erledigen - wenn es länger dauert, liegt es nicht am eigenen Rechner sondern am Netz. LWP arbeitet intern nämlich folgendermaßen: $socket = new IO::Socket ...; print $socket $request; $response = read $socket; return $response; Die Wartezeit entsteht vor allem beim Lesen der Antwort, zu kleineren Teilen beim Verbindungsaufbau und selten beim Schreiben der Anforderung. Prinzipiell gibt es auch hier eine Folge von Ereignissen, auf die gewartet wird - allerdings immer nur eines, d.h. die Ereignisse "steuern" den Programmablauf nicht. Daher ist dies keine Ereignissteuerung. Der Vorteil einer solchen Struktur ist ihre Einfachheit - die zeitliche und logische Abfolge des Requests stehen klar hintereinander. Diese Einfachheit läßt sich auch ereignisgesteuert erreichen - allerdings mit beträchtlichem Mehraufwand an anderer Stelle, den man jedoch gut in andere Module verpacken kann. =head3 blocking vs. "blocking" Ein paar Worte noch zu blocking vs. non-blocking, bzw. blocking vs. andere Arten von blocking: Unter Unix kann man Filehandles in den "non-blocking"-Zustand versetzen. Dies ist manchmal hilfreich, hat aber mit Ereignissteuerung wenig zu tun: Zum Lesen von Sockets braucht man dies nicht, und bei Dateien zeigt es keine Wirkung (es wirkt nur auf Dateihandles, bei denen die Menge an Daten unbekannt ist). In vielen Fällen ist es sogar hinderlich. So ist es bei vielen Protokollen (whois, finger, viele HTTP-Anfragen) fast sicher, daß die Anfrage komplett in den TCP-Buffer geschrieben werden kann. Passt er nicht, blockiert der Prozess, aber wenn dies unter normalen Umständen nicht vorkommt, ist dies meistens akzeptabler als die Programmstruktur nur für diesen zu verkomplizieren, wie dies bei einem non-blocking Dateihandle nötig wäre (leider erzeugt das C-Modul alle Socket-Handles per default non-blocking, was meiner Meinung nach mehr Probleme schafft als löst). In diesem Dokument wird der Begriff "blockierend" nicht als Gegensatz zu diesem C gebraucht, sondern allgemein verwendet, um jeglichen Aufruf zu beschreiben, der auf bestimmte Ereignisse warten muss, statt sofort zurückzukehren. Dies schließt nicht nur Dinge wie das Lesen von Sockets, wenn keine Daten vorhanden sind, oder sleep()-ähnliche Aufrufe ein, sondern auch Dinge wie das Lesen von einer Datei auf der Festplatte oder von NFS, wenn die Daten nicht im RAM sind. =head2 State-Maschinen Um nun auf Ereignisse in beliebiger Folge reagieren zu können, muss man jedes Ereignis ("Verbindung aufgebaut", "Daten können geschrieben werden", "Daten können gelesen werden") in einer Unterroutine abarbeiten. Bei Frameworks wie POE werden alle diese Unterroutinen als Methoden eines Objektes aufgefasst. Das Objekt speichert den Zustand (z.B. "Aufbauphase", "Antwortphase"). Bei jedem Ereignis muss der Zustand aus dem Objekt "deserialisiert" werden, die Methode aufgerufen werden und der neue Zustand wieder serialisiert werden. Für das Finger-Protokoll würde das in etwa so aussehen (völlig fiktive Syntax): # fiktive "erwarte folgendes Ereignis"-Methode: sub on_event { my ($self, $filehandle, $watch_type, $method) = @_; ... } # wird aufgerufen, wenn Daten geschrieben werden können sub can_write { my ($self) = @_; # Request blockierend schreiben, da klein print {$self->{fh}} $self->{username}; # sorge dafür, daß beim nächsten Mal can_read aufgerufen wird $self->on_event ($self->{fh}, "readable", "can_read"); } # wird aufgerufen, wenn Daten vorliegen sub can_read { my ($self) = q_; sysread $self->{fh}, $self->{response}, length $self->{response}, 8192 or $self->on_event (undef, undef, "finish"); } C schreibt den Request und sorgt dann dafür, daß der Zustand "can_read" angenommen wird, bei dem die Methode desselben Namens aufgerufen wird, sobald Daten vom Dateihandle gelesen werden können. C liest nun die Daten ein und verbleibt im aktuellen Zustand, solange Daten gelesen werden können. Bei Fehlern oder bei Verbindungsende wird in den Zustand "finish" gesprungen. Typischerweise gibt es noch andere Methoden, die z.B. bei Fehlern aufgerufen werden können. Persönlich finde ich diesen Stil äußerst masochistisch und verwirrend, aber es soll Programmierer geben, die anders denken. Für mich zählt aber vor allem die Menge an Quellcode, die erzeugt und gewartet werden muss, sowie das weite Auseinanderliegen wichtiger Stellen - der Ablauf ist nicht klar. Trotzdem ist dieser Stil häufig vertreten - meistens nicht in dieser Klarheit sondern in Kombination mit anderen Stilen (im obigen Beispiel wird z.B. die Anfrage nicht ereignisgesteuert geschrieben) - in der Realität muss man zwischen Klarheit, Geschwindigkeit, Abhängigkeiten und vielem mehr abwägen! =head2 Continuation Passing Ein etwas weniger häufig benutzter Stil ist Continuation Passing. Die Ursache für das seltenere Vorkommen liegt I nach darin, daß dies in anderen (verbreiteten) Programmiersprachen (C, Python, ...) weniger oder garnicht gut realisierbar ist, bzw. dies etwas Verständnis und Wissen erfordert, was heutzutage nicht mehr gefördert wird. Wie dem auch sei, hier ein Beispiel wie man eine Datei öffnet und einige Daten liest, und sie dann löscht, mit C und voller Fehlerbehandlung: aio_open "tempfile", O_RDONLY, 0, sub { my $fh = shift or die "tempfile: $!"; my $buf; aio_read $fh, 0, 8192, $buf, 0, sub { $_[0] > 0 # keine leeren Files or die "tempfile: $!"; aio_unlink "tempfile", sub { $_[0] and die "tempfile: $!"; print "read successfully up to 8192 bytes, and nuked file.\n"; }; }; }; Das Schema ist immer das gleiche: Bei jeder Anforderung (open/read/unlink) wird eine Closure übergeben (die Zustandsdaten und Reaktion verkörpert), die nach Beendigung der Anforderung aufgerufen wird. C kehrt sofort zurück, aber die Closure wird erst dann aufgerufen, wenn die Datei geöffnet wurde oder ein Fehler auftrat. Der Name dieses Stils kommt daher, daß bei jeder Anforderung die komplette "Fortsetzung" (Continuation) des Programmablaufes mit übergeben wird. Die Vorteile des Stils liegen darin, daß die logische (nicht zeitliche) lineare Reihenfolge klar bleibt und daß alles nahe beieinander steht. Die Nachteile sind das zunehmende Nesting und die weniger flexiblen Reaktionsmöglichkeiten. Außerdem bietet sich nicht jede Art der Anforderung gleichermaßen an: C fördert diesen Stil. Bei C wird es meistens etwas umständlicher, da mehr Parameter zu übergeben sind und die Watcher nach jedem Ereignis per Hand gelöscht werden müssen. Beides läßt sich umgehen, indem man sich in Richtung State-Maschine bewegt: sub do_open { aio_open ..., sub { do_read (@_); }; } sub do_read { my ($fh) = @_; aio_read ..., sub { do_unlink ($fh, @_); }; } sub do_unlink { ... } Das ist erstmal umständlicher, aber sehr langen Closures manchmal vorzuziehen. Der Zustand wird hier nicht in einem Objekt gespeichert, sondern in den Closures (z.B. C<$fh> bei C). =head2 Coroutinen/Threads/Prozesse Der letzte Stil besteht darin, für jeden logischen Ablauf eine Coroutine, einen Thread oder einen Prozess zu benutzen. Die Nachteile nenne ich gleich: Coroutinen sind nicht in Perl integriert und daher etwas unportabler als Perl selbst, Threads stehen in Perl überhaupt nicht zur Verfügung, und Prozesse sind relativ große Dinger, von denen man sich nicht wirklich viele halten möchte. Das obige C-Beispiel sähe mit Hilfe von C so aus: my $fh = aio_open "tempfile", O_RDONLY, 0 or die "tempfile: $!"; aio_read $fh, 0, 8192, my $buf, 0 or die "tempfile empty or read error ($!)"; aio_unlink "tempfile"; Meiner Meinung nach ist dies die klarste Methode, komplett ereignisgesteuerte Abläufe darzustellen. Der Overhead, der durch die Verwaltung von Coroutinen entsteht, ist allerdings höher als der anderer Ansätze (allerdings niedriger als bei Threads oder Prozesse), und stellt daher das andere Ende des Overhead vs. Einfachheit-Gegensatzes dar. =head1 Event-Modelle Perl strotzt geradezu vor Event-Modellen. Das ist keine gute Nachricht, da sie alle inkompatibel sind. Entstanden ist diese Situation dadurch, daß C wandern nicht oder nur halb in die Distribution. Dennoch stellt Event nahezu das Optimum dar. =head2 Glib (Gtk2) Glib implementiert das Event-Modell für Gtk+, sowie noch vieles weiteres, wie z.B. Datentypen und Klassen (was die Unterstützung für Skript-Sprachen so angenehm macht). Die drei Beispiele lauten in Glib-Lingo so: # Auf Daten warten: add_watch Glib::IO fileno $fh, ['in', 'hup'], sub { # es kann nichtblockierend gelesen werden 1 # wir wollen weitere Ereignisse dieser Art }; # Jede Stunde was tun: add Glib::Timeout 3600_000, sub { # tue was 1 # auch nächste Stunde sind wir wieder für Sie da }; # Hauptschleife: 1 while Glib::MainContext->default->iteration (1); # bzw.in Gtk2-Programmen: main Gtk2; Interessanterweise erlaubt es Glib, die eigentliche C