Aus Linux-Magazin 12/2014

So verwaltet der Perl-Programmierer asynchrone Programmflüsse effizient

© innovari, Fotolia

Asynchroner Programmfluss artet schnell in unleserlichen Code aus, falls kein übergreifendes Konzept die Struktur vorgibt. Zum Glück hat die Javascript-Gemeinde einige funktionale Tricks erfunden, die auch dabei helfen, asynchronen Perl-Code zu zähmen.

Online PLUS

In einem Screencast demonstriert Michael Schilli das Beispiel: https://www.linux-magazin.de/2014/12/plus

Sogar Experten bauen in ihren Code nicht selten unbeabsichtigt Wettlaufsituationen (Race Conditions) ein. Leicht übersehen selbst sie Seiteneffekte, wenn Multithreading im Code plötzlich dazwischenfunkt. Die Ursachen müssen Spürtrupps später mit viel Aufwand erkunden (Abbildung 1). Denn der Fehler, den der Kunde meldet, kommt nur sporadisch vor, weil er nur beim zeitlichen Zusammentreffen bestimmter Ereignisse auftritt und sich in der Entwicklungsabteilung nicht einfach reproduzieren lässt.

Abbildung 1: Asynchroner Programmfluss sorgt nicht selten für unleserlichen Code.

Abbildung 1: Asynchroner Programmfluss sorgt nicht selten für unleserlichen Code.

Ein Thread reicht

Läuft – wie etwa in einer Javascript-Anwendung – nur ein Thread, dann gibt es keine Race Conditions, denn die CPU arbeitet den Code wie aufgeschrieben ab, nichts kommt unerwartet dazwischen. So weit also ein klares Plus.

Doch damit ein Programm mit nur einem Thread ebenso schnell läuft wie eines mit vielen, darf es nicht untätig herumsitzen, während zum Beispiel eine sehr viel langsamere Netzwerkoperation läuft. Dafür gibt der Programmierer die Kontrolle in ausgewählten Programmteilen an eine Eventschleife ab, die Ereignisse dann asynchron abarbeitet. Ist das Programmierergehirn einmal auf den asynchronen Programmfluss umgestellt, schreiben sich auch komplizierte Abläufe wunderbar leicht. Aber bis es so weit ist, sind einige Hürden zu nehmen.

Hipster, hilf!

Manch einer kämpft lange damit, asynchrone Programmflüsse zu begreifen. Die Auferstehung von Javascript als Hipstersprache, besonders auf Server-Seite mit dem komplett asynchronen Framework Node.js, brachte nicht nur einige der altbekannten typischen Stolpersteine zutage. Vielmehr entstanden in letzter Zeit einige elegante Lösungen wie Pubsub, Promises oder ganze Frameworks wie Async.js, die den asynchronen Fluss auch komplexer Applikationen bändigen helfen [2].

Asynchronen Fluss empfinden Neulinge auch in Perl oft als Einschränkung, denn auf einmal können sie zum Beispiel den Inhalt einer Webseite nicht mehr einfach mit dem CPAN-Modul LWP::User-Agent und dem Aufruf

$ua->get("http://foo.com");

einholen. Während »get()« auf die Antwort des Webservers und die zäh eintrudelnden Daten wartet, stünden dann nämlich alle Räder der Applikation still. So geht es also nicht. Vielmehr läuft nun alles über Callbacks, wie im Any-Event-Framework etwa mit:

http_get("http://foo.com", sub { print $_[1] } );

Das Programm gibt dabei nur den Auftrag, die Webseite abzuholen, und kehrt sofort nach dem Aufruf der Funktion »http_get()« zum Hauptprogramm zurück. Die Eventschleife hat den Auftrag angenommen und kümmert sich ums Einholen und Aufsammeln der Daten erst dann, wenn das Programm wieder eine Pause einlegt. Falls alles komplett vorliegt, ruft die globale Eventschleife den zuvor beigefügten Callback mit den eingeholten Daten auf, der sie mit der »print« -Anweisung ausgibt.

Diese Struktur wirft den Programmfluss einer Applikation allerdings gehörig über den Haufen. Stehen zum Beispiel mehrere Webrequests hintereinander an, deren Anfragen vorher abgefragte Ergebnisse benötigen, nimmt die Verschachtelung schnell schwer zu durchschauende Ausmaße an:

http_get($url1,
   sub {
      http_get($url2, sub {
         # ...
      });
 });

Pyramide der Verdammnis

Das Callback-Verfahren führt bei unsachgemäßer Handhabung schnell zu der so genannten “Pyramid of Doom”. Dabei handelt es sich um verschachtelte Funktionen, in denen ein Callback jeweils einen weiteren Callback definiert. Irgendwann ist aber auch der breiteste Bildschirm nicht mehr breit genug, um den Programmfluss so auszuschreiben, ganz zu schweigen davon, dass derartiger Code äußerst schwer zu lesen und zu begreifen ist. Wer wollte dies bei Listing 1 bestreiten?

Listing 1

http-get-nested

01 #!/usr/local/bin/perl -w
02 use strict;
03 use AnyEvent::HTTP;
04 use CountServer;
05
06 my $cs = CountServer->new();
07 $cs->start();
08
09 my $start_url = $cs->url() . "/test-1.txt";
10
11 http_get $start_url, sub {
12   my ($body, $hdr) = @_;
13
14   print "Got: $body\n";
15   http_get $body, sub {
16       my ($body, $hdr) = @_;
17
18       print "Got: $body\n";
19       http_get $body, sub {
20         my ($body, $hdr) = @_;
21
22         print "Got: $body\n";
23       }
24   }
25 };
26
27 my $cv = AnyEvent->condvar();
28 $cv->recv();

Es simuliert das Problem, das entsteht, wenn das Ergebnis einer Webanfrage in die nächste Anfrage einfließt, wenn also mehrere Webanfragen voneinander abhängen, und zwar asynchron, aber in einer vorgegebenen Reihenfolge abgearbeitet werden müssen. Hierzu bemüht das Skript in Listing 1 mit »CountServer« einen Testserver, der auf Anfragen auf die URL »http://localhost:9090« jeweils eine weitere URL mit einer um eins hochgezählten numerischen Pfadkomponente zurückgibt:

$ curl http://localhost:9090/test-1.txt
http://localhost:9090/test-2.txt
$ curl http://localhost:9090/test-2.txt
http://localhost:9090/test-3.txt
$ curl http://localhost:9090/test-5.txt
http://localhost:9090/test-6.txt

Listing 1 startet den ersten asynchronen Webrequest mit dem String »/test-1.txt« , bekommt »/test-2.txt« zurück, den dann wiederum der zweite Request abschickt und »test-3.txt« zurückbekommt. In der Tat lautet die Ausgabe von »http-get-nested« in Listing 1:

$ ./http-get-nested
Got: http://localhost:9090/test-2.txt
Got: http://localhost:9090/test-3.txt
Got: http://localhost:9090/test-5.txt

Das bestätigt, dass der Code zwar ziemlich schwer zu verstehen, aber trotzdem durchaus funktionsfähig ist. Den Code des Countservers zeigt das Listing 2, es handelt sich um eine ganz einfache und schlichte Perl-Klasse, deren »start()« -Methode einen Webserver des CPAN-Moduls AnyEvent::HTTPD anwirft. Mit »reg_cb()« in Zeile 22 registriert der Webserver einen Handler für eingehende Anfragen und gibt damit lediglich um eins erhöhte Pfadangaben an den anfragenden Web-client zurück.

Listing 2

CountServer.pm

01 use strict;
02 use URI::URL ();
03 use AnyEvent::HTTPD;
04
05 my $PORT     = 9090;
06 my $BASE_URL = "http://localhost:$PORT";
07
08 ###########################################
09 sub new {
10 ###########################################
11     bless {}, $_[0];
12 }
13
14 ###########################################
15 sub start {
16 ###########################################
17   my( $self ) = @_;
18
19   $self->{ httpd } =
20     AnyEvent::HTTPD->new( port => $PORT );
21
22   $self->{ httpd }->reg_cb(
23     "" => sub {
24       my ($hdr, $req) = @_;
25
26       my $path = $req->url()->as_string;
27       ( my $newpath = $path ) =~
28           s/(\d+)/$1 + 1/ge;
29
30       my $url = URI::URL->new( $BASE_URL );
31       $url->path( $newpath );
32
33       $req->respond ({
34         content => ['text/txt',
35         $url->as_string] } );
36       }
37   );
38 }
39
40 ###########################################
41 sub url {
42 ###########################################
43   my( $self ) = @_;
44
45   return $BASE_URL;
46 }
47
48 1;

Wer denkt, man müsse einen externen Webserver starten, um einen Webclient zu testen, wird sich zwar verwundert am Kopf kratzen und Hexenwerk vermuten, aber in der asynchronen Welt ist es gang und gäbe, sowohl Server als auch Client zu Testzwecken gleichzeitig im selben Programm ticken zu lassen. So wird der Unit-Test zum Integration-Test!

Falsche Fehler

Doch zurück zum zwar funktionsfähigen, aber verschachtelten Clientcode: Auch auftretende Fehler behandelt die Pyramide der Verdammnis nicht ordnungsgemäß. Was passiert zum Beispiel, wenn in der zweiten Stufe ein Fehler passiert? Der Callback der ersten Stufe ist nach erfolgreichem Abschluss des Webrequests in seinen Callback eingesprungen und hat die Eventschleife aufgefordert einen weiteren Request abzusetzen. Schlägt dieser dann fehl, lässt sich nur schwer der Zusammenhang mit der ersten Stufe oder gar dem ursprünglichen Programm herstellen, das eigentlich von dem Problem Wind bekommen und Details über die Art des Fehlers und dessen Entstehungsort ausgeben sollte.

Oder wie soll das Hauptprogramm feststellen, ob bei parallel abgesetzten Webrequests schon alle Ergebnisse bereitliegen oder ob immer noch ein paar fehlen? Oder wie den Programmfluss unterbrechen, sobald ein Request fehlschlägt, und die folgenden ignorieren? Kontrolle über den Programmfluss sollte auch in der asynchronen Programmierung gegeben sein, und genau dieses Problem schickten sich Javascript-Programmierer in den letzten Jahren an mittels mehrerer konkurrierender Ansätze zu lösen.

Pubsub

Softwarekomponenten, die miteinander durch gesendete und empfangene Events kommunizieren, sind in der Lage, die Pyramide der Verdammnis zu sprengen. Zum Einsatz kommt ein Publish/Subscribe-Modell, in dem Objekte sich an eingehenden Events interessiert zeigen und, falls sie diese empfangen, Methoden anwerfen. Zusätzlich bieten sie Methoden an, die es anderen Objekten erlauben, diese Events zu senden.

Das Ganze läuft asynchron ab, denn ein gesendetes Event verarbeitet das Zielobjekt nicht sofort, sondern erst, wenn es dazu Zeit findet. Der Sender wartet auch nicht synchron auf ein sofort zu lieferndes Resultat, sondern kehrt zu seinen eigenen Angelegenheiten zurück und achtet nebenbei auf ein eingehendes Event, das das Ergebnis der Anfrage enthält.

Das CPAN-Modul Object::Event bietet für Perl eine simple Implementierung des Verfahrens. Eine Klasse erbt von Object::Event und registriert dann mit der Methode »reg_cb()« (für “register callback”) einen Callback zu einem vorgegebenen Event. Dieses liefert später die ebenfalls geerbte Methode »event()« an, aufgerufen von externen Objekten, die Aktionen im Objekt ihrer Begierde auszulösen trachten.

Listing 3 definiert hierzu zunächst einmal eine Klasse »WgetPubSub« , deren Objekte im Konstruktor einen Callback auf das Event »request« registrieren. Die mit dem Event eintrudelnde URL holen sie sich aus dem Web und schicken das Ergebnis mit Hilfe eines neuen Events mit dem Namen »response« wieder zurück. In Zeile 40 hat der Code (zeitlich) vorher mittels »reg_cb()« einen Callback auf »response« definiert, schnappt sich dann das übermittelte Ergebnis und leitet einen neuen Webrequest durch ein weiteres Pubsub-Objekt »$wget2« ein.

Listing 3

http-get-pubsub

01 #!/usr/local/bin/perl -w
02 package WgetPubSub;
03 use base 'Object::Event';
04 use AnyEvent::HTTP;
05
06 ###########################################
07 sub new {
08 ###########################################
09   my $self = bless {}, $_[0];
10
11   $self->reg_cb( "request", sub {
12     my ( $c, $url ) = @_;
13
14     http_get( $url, sub {
15       my ($body, $hdr) = @_;
16
17       $self->event( "response", $body );
18     } );
19
20
21   } );
22
23   return $self;
24 }
25
26 package main;
27 use strict;
28 use AnyEvent::HTTP;
29 use CountServer;
30
31 my $cs = CountServer->new();
32 $cs->start();
33
34 my $start_url = $cs->url() . "/test-1.txt";
35
36 my $wget  = WgetPubSub->new();
37 my $wget2 = WgetPubSub->new();
38 my $wget3 = WgetPubSub->new();
39
40 $wget->reg_cb( "response", sub {
41   my( $c, $body ) = @_;
42
43   print "Got: $body\n";
44   $wget2->event( "request", $body );
45 } );
46
47 $wget2->reg_cb( "response", sub {
48   my( $c, $body ) = @_;
49
50   print "Got: $body\n";
51   $wget3->event( "request", $body );
52 } );
53
54 $wget3->reg_cb( "response", sub {
55   my( $c, $body ) = @_;
56
57   print "Got: $body\n";
58 } );
59
60 $wget->event( "request", $start_url );
61
62 my $cv = AnyEvent->condvar();
63 $cv->recv();

Der Code ist nicht verschachtelt, sondern linear aufgedröselt, was die Lesbarkeit erhöht, und es bleibt dem Programmierer vorbehalten, die einzelnen Objekte zur Weiterverarbeitung an weitere Funktionen oder gar fremde Pakete zur Weiterverarbeitung zu schicken, wenn dies der logischen Aufteilung dient.

Mit Pubsub geht auch die Fehlerbehandlung einfacher von der Hand. Da die zweite Stufe des Webrequests ohne Weiteres über die Variable »$wget« auf den Webagenten der ersten Stufe zugreifen kann, ist es ihr ebenso möglich, diesem im Fehlerfall auch eine Nachricht zu schicken, zum Beispiel ein Event mit dem Namen »error« .

Lauscht die Komponente auf diese Events, bekommt sie von dem Problem, das sich weiter hinten ereignet hat, Wind und kann ihrerseits eine Komponente im Hauptprogramm benachrichtigen.

Leere Versprechen

Eine vielversprechende neue Methode bei der asynchronen Programmierung sind Promises [3]. Dabei gibt eine asynchrone Funktion mit einem Callback ein abstraktes Objekt vom Type Promise zurück, eine Art Fenster in die Zukunft. Dabei ist zunächst noch nicht bekannt, ob das Promise irgendwann einmal ein Ergebnis zugespielt bekommt oder ob es einen Fehler verursacht. Bis zur Entscheidung ist es eine Art hybrides, fremdgesteuertes Zwitterwesen, das darauf wartet, dass irgendjemand eine Entscheidung trifft, einen Schalter umlegt und eventuell Ergebnisdaten hineinpumpt.

Am einfachsten erklärt ein Beispiel die Funktion eines Promise. Listing 4 definiert in Zeile 5 ein Deferred-Objekt, eine Art Promise mit Entscheidungsgewalt. Aus ihm leitet Zeile 7 ein Promise ab, dem die »then()« -Methode zwei alternative Zustände einhaucht: Einmal ist Schrödingers Katze am Leben, einmal ist sie tot. Beide sind offensichtlich unvereinbar, nur einer von beiden kann sich irgendwann bewahrheiten.

Listing 4

promise-cat

01 #!/usr/local/bin/perl -w
02 use strict;
03 use Promises qw( deferred );
04
05 my $schroedingers_cat = deferred();
06
07 $schroedingers_cat->promise->then(
08   sub { print "The cat is alive.\n" },
09   sub { print "The cat is dead.\n" },
10 );
11
12 $schroedingers_cat->resolve();
13   #  "The cat is alive."
14   # or: $schroedingers_cat->reject();
15   #   "The cat is dead."

Der Zeitpunkt der Entscheidung kommt in dem Augenblick, in dem das übergeordnete Deferred-Objekt eine von zwei Methoden aufruft: Ruft es »resolve()« auf, bewahrheitet sich der erste Zustand, ruft es »reject()« , wird der zweite Wirklichkeit. Ist der Schalter umgelegt, gibt es kein Zurück mehr, der Zustand des Promise ist dann festgelegt.

Der Unterschied zwischen einem Deferred und einem Promise ist nur der Zugriff auf die Methoden »reject()« und »resolve()« . Ein Deferred kann diese auslösen und damit das Schicksal des von ihm abgeleiteten Promise besiegeln. Ein Promise kann nur reagieren, nicht selbst entscheiden.

Im Falle einer asynchronen Funktion, die ein Promise zurückgibt, ist dieses zunächst noch nicht auf ein Ergebnis oder eine Fehlermeldung festgelegt. Den Schalter für die Entscheidung bedient später der Callback, wenn Daten eintrudeln oder ein Fehler vorliegt. Das passiert aber erst später, nachdem der gegenwärtige Programmfluss die Kontrolle an die Eventschleife abgegeben hat. Any Event erlaubt es einem Promise, die Instruktionen für den »resolve« -Callback beziehungsweise den »reject« -Callback auch erst nach dem Aufruf der asynchronen Funktion auszuführen, denn vorher liegt noch kein Ergebnis vor.

Listing 5 wandelt die Funktion »http_get()« aus dem Fundus des Any-Event-Moduls in Zeile 10 zunächst in eine Funktion »fetch_url()« um, die eine URL erwartet und ein Promise zurückgibt. Der Callback, den »http_get()« aufruft, falls Webdaten oder ein Fehler vorliegen, legt später dann den Schalter mit »resolve()« oder »reject()« um.

Listing 5

http-get-promise

01 #!/usr/local/bin/perl -w
02 use strict;
03 use AnyEvent::HTTP;
04 use CountServer;
05 use Promises qw[ deferred ];
06
07 my $cs = CountServer->new();
08 $cs->start();
09
10 sub fetch_url {
11   my ($url) = @_;
12   my $d = deferred;
13   http_get $url => sub {
14     my ($body, $headers) = @_;
15     $headers->{Status} == 200
16     ? $d->resolve( $body )
17     : $d->reject( $headers->{Reason} )
18   };
19   $d->promise;
20 }
21
22 my $start_url = $cs->url() . "/test-1.txt";
23
24 my $prom1 = fetch_url( $start_url );
25
26 my $prom2 = $prom1->then( sub {
27   my( $body ) = @_;
28   print "Got: $body\n";
29   return fetch_url( $body );
30 });
31
32 my $prom3 = $prom2->then( sub {
33   my( $body ) = @_;
34   print "Got: $body\n";
35   return fetch_url( $body );
36 });
37
38 $prom3->then( sub {
39   my( $body ) = @_;
40   print "Got: $body\n";
41 });
42
43 my $cv = AnyEvent->condvar();
44 $cv->recv();

Zeile 26 definiert mit der Methode »then()« den Callback für einen erfolgreichen Webrequest und macht sich die Eigenschaft von Promises nach der neuesten Promise/A+-Spezifikation [4] zunutze, dass ein solcher Callback wiederum ein Promise zurückgeben darf. Dies schnappt sich »$prom2« und geht ab Zeile 32 in die dritte Runde.

Die Ausgabe von Listing 5 ist wieder identisch mit der der Listings 1 und 3, alle drei Skripte fragen die gleichen URLs ab und erhalten die gleichen Antworten vom Server. Da die »resolve« – oder »reject« -Callbacks und damit die »then()« -Methode wiederum ein Promise zurückgeben, lässt sich die Kette der Requests auch ohne temporäre Variablen als

->then( sub { # success
 } )
->then( sub { # success
 } )-> [...]

schreiben. Das exakt nach der Spezifikation programmierte Promises-Modul stellt dann sicher, dass es die ganze Kette abarbeitet und sofort damit aufhört, falls in einem Glied ein Fehler mit einem »reject« -Aufruf auftritt.

Auch in diesem Beispiel ist der Code schließlich einfacher zu lesen als in der ursprünglichen, verschachtelten Callback-Pyramide. Und wieder einmal fand eine moderne Programmiertechnik aus einer völlig anderen Sprache zurück in das gute alte Perl.

Infos

  1. Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/ magazin/2014/12/Perl
  2. Trevor Burnham, “Async JavaScript: Build More Responsive Apps with Less Code”: Pragmatic Express, 2012
  3. Domenic Denicola, “You’re mising the point of promises”: http://domenic.me/2012/10/14/youre-missing-the-point-of-promises
  4. Promises/A+-Spezifikation: https://promisesaplus.com

Der Autor

Michael Schilli arbeitet als Software-Engineer bei Yahoo in Sunnyvale, Kalifornien. In seiner seit 1997 erscheinenden Kolumne forscht er jeden Monat nach praktischen Anwendungen der Skriptsprache Perl. Unter mailto:mschilli@perlmeister.com beantwortet er gerne Fragen.

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 5 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben