POE, das ``Perl Object Environment'', bietet kooperatives Multi-Tasking ohne dass mehrere Prozesse oder Threads die Applikation verkomplizieren. Zusammen mit GTK, dem Grafik-Toolkit des Gimp lässt sich so ein simpler aber ruckfrei operierender Aktienticker mit graphischer Oberfläche implementieren.
Graphische Oberflächen arbeiten typischerweise Event-basiert: In einer Hauptschleife wartet das Programm auf Ereignisse wie Mausklicks oder Tastatureingaben des Benutzers. Es ist wichtig, dass das Programm diese Events verzögerungsfrei bearbeitet und sofort wieder in die Haupt-Eventschleife eintritt -- sonst ist die Oberfläche ``tot'' und der Benutzer kriegt den Rappel.
Abbildung 1: Der Gtk-basierte Aktienticker in Aktion |
Die heute vorgestellte Aktienticker-Applikation (Abbildung 1) kontaktiert in regelmäsigen Abständen die Yahoo-Finance-Webseite, um die neuesten Börsenkurse einzuholen. So ein Webrequest kann je nach Internetwetterlage inklusive der DNS-Auflösung des Servernamens schon schon mal ein paar Sekunden dauern -- während dieser Zeit muss allerdings die Oberfläche der Applikation weiterackern.
Derartige Anforderungen kann man mit Multi-Processing oder Multi-Threading lösen. Damit erhöht sich allerdings die Komplexität eines Programms beträchtlich: Kritische Sektionen müssen vor parallelen Zugriffen geschützt werden, um die Integrität der Daten zu gewährleisten und es schleichen sich oft schwer zu analysierende und kaum zu reproduzierende Fehler ein. Wer sich schon einmal die Haare gerauft hat, weil er einen Core-Dump mit 200 laufenden Threads untersuchen musste, weiss, wovon ich rede.
Eine weitere Möglichkeit ist kooperatives Multitasking mit POE, dem Perl Object Enviroment. In dieser als ``State-Machine'' implementierten Umgebung läuft zu jedem Zeitpunkt genau ein Prozess mit nur einem Thread, aber ein auf Benutzerebene realisierter ``Kernel'' sorgt dafür, dass verschiedene Aufgaben scheinbar gleichzeitig abgearbeitet werden.
Um Aktienpreise einzuholen, nimmt man in Perl üblicherweise das CPAN-Modul Yahoo::FinanceQuote, aber das hat wegen unserer Ruckfrei-Anforderung genau ein Problem: Es arbeitet synchron.
use Finance::YahooQuote; my @quote = getonequote($symbol);
Die Funktion getonequote
setzt einen HTTP-Request an den Yahoo-Server
ab, wartet auf die Antwort und kehrt erst dann mit einem Ergebnis zurück.
Während der Wartezeit hätten wir allerdings lieber unsere Oberfläche
in Schuss gehalten -- wie's der Teufel will hat womöglich gerade jemand
ein anderes Fenster über den Ticker gezogen, sodass dieser einen Teil seines
Zuständigkeitsbereichs
neu zeichnen muss. Aber der gerade laufende Thread zieht es vor,
untätig herumzusitzen, was ein gräuliches Loch auf dem Desktop des
Benutzers erzeugt. So geht's nicht.
Es wäre geschickter, einen Web-Request auszuschicken, und uns, ohne auf das Ergebnis zu warten, einfach wieder um die graphische Oberfläche zu kümmern. Kommt die Antwort vom Yahoo-Server, wollen wir geweckt werden, schnell das Aktientickerfenster aktualisieren und ebenfalls sofort wieder zurück in die GUI-Hauptschleife springen.
Genau dies erledigt das POE-Framework. Es besteht aus einem ``Kernel'', in dem einzelne Applikationen ``Sessions'' registrieren. Dort springen State-Maschinen von Zustand zu Zustand und schicken sich gegenseitig Nachrichten. I/O-Aktivitäten erfolgen asynchron: Statt blockierend über ein File-Handle Daten einzulesen, sagt man: ``Hey, Kernel, ich will die Datei einlesen, wecke mich, wenn die Daten verfügbar sind.'' Zwar findet kein ``richtiges'' asynchrones Schreiben oder Lesen statt (POE nutzt unter der Haube lediglich nicht-blockierende syswrite/sysread-Funktionen), aber die bereitstehenden Häppchen werden mit Volldampf rausgepustet oder reingepfiffen.
Der ``kooperative'' Aspekt bei POE ist, dass die einzelnen Sessions sich darauf verlassen, dass niemand herumtrödelt, sondern sofort, wenn eine Aufgabe den Prozessor nicht voll auslastet, die Kontrolle freiwillig an den Kernel zurückgibt. Eine einzige unkooperative Stelle im Programm, und schon leidet das ganze System.
Dieses Multi-Tasking mit einem einzigen Thread erleichtert die Programmentwicklung erheblich -- kein Lock muss her, keine bösen Überraschungen mit Race-Conditions passieren, und wenn doch mal ein Fehler passiert, kann man ihn üblicherweise einfach aufspüren.
Und POE arbeitet auch schön mit den Haupt-Event-Schleifen einiger graphischer Umgebungen zusammen: Perl/Tk und Gtkperl erkennt POE automatisch und bindet sie und ihre Anforderungen nahtlos in den kooperativen Reigen ein.
Um den Aktienticker zu implementieren, wird ein Zustandsautomat
POE::Session nach Abbildung 2 benötigt. Nach dem Initialisierungszustand
_start
, der unter anderem die Gtk-Oberfläche aufbaut und
den Alias-Namen ticker
setzt, damit wir die Session später leicht
identifizieren können, geht die
Kontrolle an den Kernel. Alle 60 Sekunden (per Alarm)
oder sofort, wenn jemand den
``Update''-Knopf der Oberfläche drückt, kommt der ``wake_up'' Zustand an
die Reihe, der einen weiteren Zustandsautomaten vom Typ
POE::Component::Client::HTTP anstösst und dann sofort wieder die Kontrolle
an den Kernel zurückgibt. PoCoCli::HTTP ist eine sogenannte Komponente
(Component) aus dem POE-Framework, ein Zustandsautomat,
der seine eigene Session
(im Listing "useragent"
genannt) definiert, im request
-Zustand
Web-Anfragen entgegennimmt und dann im
POE-Framework mitspielt,
bis er eine HTTP-Antwort vollständig erhalten hat.
Dann teilt er dem Kernel
mit, dass die Session, die ihn aufgerufen hat (ticker
), einen ihm
vorher mitgeteilten
Zustand (yhoo_response
) anspringen soll. Veranlasst der Kernel
die ticker
Session dazu,
nimmt sie die bereitliegende HTTP-Antwort entgegen,
frischt die Aktien-Widgets in der Anzeige auf und gibt die Kontrolle
sofort wieder an den Kernel zurück.
Ab Zeile 59 startet die POE::Component::Client::HTTP
-Komponente mit
spawn()
und legt fest, dass beim Server UserAgent-String
gtkticker/0.01
auftaucht und und hängende Anfragen nach 60 Sekunden
abgebrochen werden.
Abbildung 2: Die POE::Session des gtktickers. |
In Listing gtkticker
definiert Zeile 9 den URL von Yahoos Aktienkursservice. Dessen CGI-Schnittstelle nimmt einen Formatparameter (f=
) mit
den geforderten Feldern (s
: Symbol, l1
: Aktienkurs c1
:
Veränderung in Prozent seit dem letzten Börsentag) und einen
Symbolparameter entgegen, der die Börsenkürzel der geforderten
Aktiengesellschaften kommasepariert enthält (z.B. "YHOO,MSFT,TWX"
).
In der Antwort des Yahoo-Servers steht dann etwas wie
"YHOO",45.38,+0.35 "MSFT",27.56,+0.19 "TWX",18.21,+0.75
was gtkticker
einfach zeilenweise und an den Kommas auseinandernimmt
und in die grafische Oberfläche einschleust.
Zeile 11 legt mit .gtkicker
im Heimatverzeichnis des
Benutzers die Datei fest, in der die vom Ticker anzuzeigenden
Symbole stehen. Die Zeilen 25 bis 31 lesen sie ein und verwerfen
mit '#' beginnende Kommentarzeilen. Die implizite for-Schleife
... for /(\S+)/g;
führt den links von ihr stehenden Ausdruck für alle Wörter einer
Zeile aus und setzt jeweils das Börsensymbol in $_ -- so dürfen
auch mehrere Symbole durch Leerzeichen getrennt in einer Zeile stehen.
Listing 1 zeigt zeigt eine Beispieldatei. Trotz POE-Frameworks verwendet
gtkticker
hier die regulären synchronen I/O-Funktionen, denn die
Konfigurationsdatei ist kurz und der Kernel läuft noch gar nicht.
Zeile 33 definiert den Zustandsautomaten des Tickers.
Der Parameter inline_states
weist mit einer Hashreferenz
den Zuständen Funktionen zu, die der Kernel anspringt, falls sie
erreicht sind. Dann schiebt Zeile 44 mit
$poe_kernel->post("ticker", "wake_up");
über die von POE
exportierte Variable $poe_kernel
den Zustand ``wake_up'' für die ``ticker''-Session in den Kernel und Zeile
45 startet mit
$poe_kernel->run();
die Kernel-Hauptschleife, die das Programm bis zum Shutdown nie mehr verlässt. Das war's!
Die vorher gezeigte Konstruktion des POE::Session
-Objekts hatte
noch einen Seiteneffekt: Die dem _start
-Zustand zugewiesene
und ab Zeile 48 definierte Routine start()
wurde ausgeführt. Sie
setzt den Alias-Namen der Session auf ticker
, und springt sodann
in my_gtk_init()
, eine ab Zeile 82 definierte Funktion, die
die Gtk-Oberfläche zusammenbaut.
Gtk
ist ein CPAN-Modul von Marc Lehmann (der freundlicherweise diesen
Artikel korrekturlas!), und ist eigentlich schon von Gtk2 abgelöst.
Allerdings spielen POE und Gtk2 noch nicht so recht zusammen, und
das ehrwürdige Gtk erledigte den Job hervorragend.
Ein Objekt der Klasse Gtk::Window
ist das Hauptfenster der
Applikation, in dem oben ein typisches Pull-Down-Menü hängt. Dieses besteht
aus einem Menübalken mit einem Eintrag File
, dessen ausziehbares
Menü nur den Eintrag Quit
enthält, der die Applikation mit
Gtk->exit(0)
beendet. Dass der Benutzer auch mit
der Tastenkombination CTRL-Q
das Programm verlassen kann, dafür
sorgt ein Gtk::AccelGroup
-Objekt, das die Menüsteuerung mit
sogenannten Accelerators bestückt.
Aufgebaut wird das Menü mittels einer Gtk::ItemFactory
,
die zunächst einen Menübalken vom Typ Gtk::MenuBar
erzeugt
und dort mittels create_items()
die Einträge und hängt ihm untergeordnete
ausklappbare Menüs ein. Der path
-Parameter gibt dabei einfach die Lage
des Menüpunktes an -- so spezifiziert /_File/_Quit
den
den Eintrag Quit
unter dem File
-Eintrag im Menübalken.
Der callback
-Parameter setzt eine Funktion, die Gtk
anspringt, falls der Benutzer den Eintrag mit der Maus anwählt oder
die über den accelerator
-Parameter definierte Tastenkombination
anspringt.
Um Widgets geometrisch anzuordnen, kommen zwei verschiedene Verfahren
zum Einsatz: Gtk::VBox
und Gtk::Table
.
Das Container-Element Gtk::VBox
ordnet in ihm enthaltene
Widgets vertikal an. Seine pack_start()
-Methode platziert dabei
die Elemente vom oberen Rand nach unten, während pack_end()
seine
Widgets von unten nach oben packt.
Der Aufruf
$vb->pack_start($mb, $expand, $fill, $padding);
packt den Menübalken $mb (dessen Widget gtkticker
in Zeile 108 per
Namenseintrag mit $factory->get_widget('<main>')
aus
der Factory holt) oben in die VBox. $expand
gibt an, ob die Fläche,
in der das Widget ``schwimmt'', sich vergrößert, falls
das Hauptfenster mit der Maus vergrößert wird. Falls ja, gibt
$fill
an, ob das Widget sich selbst ausdehnt -- so können kleine
Druckknöpfe riesengroß werden. Und $padding
schließlich spezifiziert
die Anzahl der Pixel, die das Widget mindestens vertikal zu seinen Nachbarn
hält.
Statusmeldungen zeigt gtkticker
in einem unauffälligen
Gtk::Label
-Widget direkt
über dem Update
-Knopf an. Die set_alignment()
-Methode gibt mit
$STATUS->set_alignment(0.5, 0.5);
an, dass der Text horizontal und vertikal zentriert wird. Ein Wert von 0.0 wäre hingegen links, 1.0 rechtszentriert.
Das Container-Element Gtk::Table
hingegen erlaubt es, andere Widgets
bequem in Tabellenform zu arrangieren. Die attach_defaults()
-Methode
nimmt das anzuordnende Widget entgegen und jeweils zwei Spalten- und zwei
Reihenkoordinaten, zwischen denen das Widget liegen soll.
$table->attach_defaults($label, 0, 1, 1, 2);
Legt zum Beispiel fest, dass das mit $label
referenzierte
Gtk::Label
-Objekt in der ersten Reihe (``zwischen 0 und 1'') und
in der zweiten Spalte (``zwischen 1 und 2'') der Tabelle $table
aufgehängt ist.
Widgets vom Typ Gtk::Button
kann man Aktionen zuordnen, die Gtk ausführt,
falls der Knopf vom Benutzer gedrückt wird. Die in Zeile 144
aufgerufene Methode signal_connect()
legt fest, dass Gtk einen
"wake_up"
-Event an den POE-Kernel schickt, falls der Benutzer auf den
Update
-Knopf klickt. Auch das Hauptfenster verknüpft eine Aktion
mit dem Ergeignis, das der Benutzer auslöst, wenn er auf das ``X'' rechts
oben klickt, um das Fenster zu schließen:
$w->signal_connect('destroy', sub {Gtk->exit(0)});
Dies räumt die Gtk-Session auf und veranlasst das Programm zum Abbruch.
Sind alle Widgets definiert, befördert die show_all()
-Methode
des Hauptfensters in Zeile 148 sie alle auf den Bildschirm.
Im Zustand yhoo_response
springt der POE-Kernel die ab Zeile
152 definierte Funktion resp_handler an. Per Definition legt
POE::Component::Client::HTTP
dabei ein Request- und Response-Paket
in ARG0
und ARG1
ab. POE nutzt ja diese seltsam anmutende
Parameterübergabe, nachdem es neue numerische Konstanten wie KERNEL,
HEAP, ARG0, ARG1 einführt und dann erwartet, dass der Programmierer
sie nutzt, um den Funktions-Parameter-Array @_
zu indizieren:
$_[KERNEL] gibt so zum Beispiel immer das Kernel-Objekt zurück.
Die erwähnten Request- bzw. Response-Pakete sind wiederum Referenzen auf
HTTP::Request bzw. HTTP::Response-Objekte, an denen wir eigentlich
interessiert sind, also extrahiert der map
-Befehl in den Zeilen
154 und 155 diese nach $req
und $resp
.
Im Fall eines HTTP-Fehlers setzt Zeile 159 eine entsprechende Meldung im Status-Widget und kehrt sofort zurück. Andernfalls wird der globale zweidimensionale Array der Label-Widgets aufgefrischt, die für jede zu überwachende Aktie das Börsensymbol, den aktuellen Kurs und die prozentuale Veränderung anzeigen. Ist die Veränderung 0, wird sie weggelassen.
wake_up
-Events im POE-Kernel lösen die ab Zeile 185 definierte
Routine wakeup_handler()
aus. Sie ruft die ab Zeile 67 definierte
Funktion upd_quotes()
auf, die ein HTTP::Request-Objekt definiert
und es per Event an die Komponente POE::Component::Client::HTTP
schickt. Als Zielzustand für den ticker
gibt sie dabei yhoo_response
an.
Nachdem dies erledigt ist, setzt wakeup_handler()
mit der
delay()-Methode
des Kernels einen Weckruf, der nach der in
$UPD_INTERVAL
definierten Sekundenzahl (60) einen wake_up
-Event
der ticker
-Session auslöst. So frischt der Ticker alle 60 Sekunden
seine Aktiendaten auf, auch wenn der Benutzer nicht den Update-Knopf drückt.
Die erforderlichen POE-Module POE
und POE::Component::Client::HTTP
installiert man am besten mit einer CPAN-Shell.
Falls das Modul POE::Component::Client::DNS ebenfalls installiert ist,
werden sogar DNS-Anfragen asynchron bearbeitet, sonst kann das eingesetzte
gethostbyname()
eine kleine Verzögerung verursachen.
Das Modul Gtk
vom CPAN zieht noch einige
weitere Abhängkeiten herein und bereitete
bei meiner Installation einige Probleme. Aber ein
touch ./Gtk/build/perl-gtk-ref.pod perl Makefile.PL --without-guessing
im Distributionsverzeichnis
mit anschließendem make install
löste das Problem.
Und wie immer lässt sich die Geschwätzigkeit der Logs auf dem Terminal mit Log::Log4perl (ebenfalls vom CPAN) und in Zeile 22 einstellen.
Es ist schon faszinierend, wie glatt sich die Oberfläche bedienen lässt. Selbst wenn man während eines automatischen Auffrischvorgangs über ein langsames Netzwerk im Menü herumfuhrwerkt, kommt die Applikation nicht ins Schleudern. Die ideale Technologie für alle Arten von grafischen Client-Applikationen!
1 # ~/.gtkticker 2 TWX 3 MSFT 4 YHOO AMZN RHAT 5 DODGX 6 JNJ COKE IBM SUN
001 #!/usr/bin/perl 002 ########################################### 003 # gtkticker 004 # Mike Schilli, 2004 (m@perlmeister.com) 005 ########################################### 006 use warnings; 007 use strict; 008 009 my $YHOO_URL = "http://quote.yahoo.com/d?". 010 "f=sl1c1&s="; 011 my $RCFILE = "$ENV{HOME}/.gtkticker"; 012 my @LABELS = (); 013 my $UPD_INTERVAL = 1800; 014 my @SYMBOLS; 015 016 use Gtk; 017 use POE qw(Component::Client::HTTP); 018 use HTTP::Request; 019 use Log::Log4perl qw(:easy); 020 use Data::Dumper; 021 022 Log::Log4perl->easy_init($DEBUG); 023 024 # Read config file 025 open FILE, "<$RCFILE" or 026 die "Cannot open $RCFILE"; 027 while(<FILE>) { 028 next if /^\s*#/; 029 push @SYMBOLS, $_ for /(\S+)/g; 030 } 031 close FILE; 032 033 POE::Session->create( 034 inline_states => { 035 _start => \&start, 036 _stop => sub { INFO "Shutdown" }, 037 yhoo_response => \&resp_handler, 038 wake_up => \&wake_up_handler, 039 } 040 ); 041 042 my $STATUS; 043 044 $poe_kernel->post("ticker", "wake_up"); 045 $poe_kernel->run(); 046 047 ########################################### 048 sub start { 049 ########################################### 050 051 DEBUG "Starting up"; 052 053 $poe_kernel->alias_set( 'ticker' ); 054 055 my_gtk_init(); 056 057 $STATUS->set("Starting up"); 058 059 POE::Component::Client::HTTP->spawn( 060 Agent => 'gtkticker/0.01', 061 Alias => 'useragent', 062 Timeout => 60, 063 ); 064 } 065 066 ########################################### 067 sub upd_quotes { 068 ########################################### 069 070 my $request = HTTP::Request->new( 071 GET => $YHOO_URL . 072 join ",", @SYMBOLS); 073 074 $STATUS->set("Fetching quotes"); 075 076 $poe_kernel->post('useragent', 077 'request', 'yhoo_response', 078 $request); 079 } 080 081 ######################################### 082 sub my_gtk_init { 083 ######################################### 084 085 my $w = Gtk::Window->new(); 086 $w->set_default_size(150,200); 087 088 # Create Menu 089 my $accel = Gtk::AccelGroup->new(); 090 $accel->attach($w); 091 my $factory = Gtk::ItemFactory->new( 092 'Gtk::MenuBar', "<main>", $accel); 093 094 $factory->create_items( 095 { path => '/_File', 096 type => '<Branch>', 097 }, 098 { path => '/_File/_Quit', 099 accelerator => '<control>Q', 100 callback => 101 [sub { Gtk->exit(0) }], 102 }); 103 104 my $vb = Gtk::VBox->new(0, 0); 105 my $upd = Gtk::Button->new( 106 'Update'); 107 108 $vb->pack_start($factory->get_widget( 109 '<main>'), 0, 0, 0); 110 111 # Button at bottom 112 $vb->pack_end($upd, 0, 0, 0); 113 114 # Status line on top of buttons 115 $STATUS = Gtk::Label->new(); 116 $STATUS->set_alignment(0.5, 0.5); 117 $vb->pack_end($STATUS, 0, 0, 0); 118 119 my $table = Gtk::Table->new( 120 scalar @SYMBOLS, 3); 121 $vb->pack_start($table, 1, 1, 0); 122 123 for my $row (0..@SYMBOLS-1) { 124 125 for my $col (0..2) { 126 127 my $label = Gtk::Label->new(); 128 $label->set_alignment(0.0, 0.5); 129 push @{$LABELS[$row]}, $label; 130 131 $table->attach_defaults($label, 132 $col, $col+1, $row, $row+1); 133 } 134 135 } 136 137 $w->add($vb); 138 139 # Destroying window 140 $w->signal_connect('destroy', 141 sub {Gtk->exit(0)}); 142 143 # Pressing update button 144 $upd->signal_connect('clicked', 145 sub { DEBUG "Sending wake_up"; 146 $poe_kernel->post( 147 'ticker', 'wake_up')} ); 148 $w->show_all(); 149 } 150 151 ########################################### 152 sub resp_handler { 153 ########################################### 154 my ($req, $resp) = 155 map { $_->[0] } @_[ARG0, ARG1]; 156 157 if($resp->is_error()) { 158 ERROR $resp->message(); 159 $STATUS->set($resp->message()); 160 return 1; 161 } 162 163 DEBUG "Response: ", $resp->content(); 164 165 my $count = 0; 166 167 for(split /\n/, $resp->content()) { 168 my($symbol, $price, $change) = 169 split /,/, $_; 170 chop $change; 171 $change = "" if $change =~ /^0/; 172 $symbol =~ s/"//g; 173 $LABELS[$count][0]->set($symbol); 174 $LABELS[$count][1]->set($price); 175 $LABELS[$count][2]->set($change); 176 $count++; 177 } 178 179 $STATUS->set(""); 180 181 1; 182 } 183 184 ########################################### 185 sub wake_up_handler { 186 ########################################### 187 DEBUG("waking up"); 188 189 # Initiate update 190 upd_quotes(); 191 192 # Re-enable timer 193 $poe_kernel->delay('wake_up', 194 $UPD_INTERVAL); 195 }
Michael Schilliarbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat "Goto Perl 5" (deutsch) und "Perl Power" (englisch) für Addison-Wesley geschrieben und ist unter mschilli@perlmeister.com zu erreichen. Seine Homepage: http://perlmeister.com. |