In the Code: was ist reaktive Programmierung?

Eine Mailbox hat 3 "Meldungen" drin (dargestellt durch 3 blaue Rechtecke). Jeder Verarbeitungsschritt wartet hinter einer Mailbox, liest Mails und schickt Mails. Hier ist ein schematischer Webserver dargestellt.
Seit einiger Zeit macht der Begriff "reaktive Programming" die Runde. Dabei reagiert die meiste Software bereits heute auf externe Signale wie den Klick eines Users oder geänderte Daten. Was steckt also dahinter? Elca erklärt die reaktive Programmierung und wann sie angewendet wird.
 
Das "Reactive Manifest", entwickelt von Szene-Grössen aus der Softwareentwicklung, teilweise aus dem Skala-Umfeld, definiert den Begriff "reaktives System". Es will die neuen Anforderungen von heutigen Softwaresystemen abdecken, die auf ganz unterschiedlicher Hardware von mobilen Devices bis zu grossen Cloud-Systemen laufen. Herausforderung: Die Benutzer erwarten rasche Antwortzeiten, eine hundertprozentige Verfügbarkeit und die Datenmengen werden immer grösser.
 
Das Manifest sieht die folgenden vier Charakteristiken für Softwaresysteme vor:
  • event-driven (message driven)
  • responsive
  • resilient (fehlertolerant)
  • elastic (skalierbar)
Für die reaktive Programmierung selbst gibt es verschiedene Definitionen. Ich wähle hier die meiner Meinung nach prägnanteste: "Reaktive Programmierung ist Programmierung mit asynchronen Datenströmen (data streams)." Im Folgenden daher eine kurze Erklärung zu diesen Begriffen.
 
Was sind Datenströme und wo sind diese einsetzbar?
Als Datenstrom kann beispielsweise die Abfolge von Mausebewegungen, Klicks und Tastatureingaben in einer Nutzer-Schnittstelle (Graphical User Interface, GUI) gesehen werden. Mit dieser Terminologie, würde ein reaktives Programm dann auf diese Datenströme reagieren. Es könnte zum Beispiel die angezeigte Information im GUI-Fenster aufgrund dieser Datenströme anpassen. Ein anderer Datenstrom könnte gleichzeitig von Änderungen an der aktuellen Datenbank kommen (z.B. laufend ankommende Kundenbestellungen). Man könnte sich also gut einen Datenstrom mit User-Interaktionen vom GUI zum Server und einen zweiten von Updates an Kundenbestellungen vorstellen. Der Benutzer kann durch die Daten navigieren und Änderungen anbringen und er wird auch informiert, wenn neue Kundenbestellungen ankommen.
 
Ein Gegenteil von solchen rasch ändernden Datenströmen sind nächtliche Batch-Verarbeitungen, bei welchen die Daten schon beim Start alle bereitliegen. Ein Reaktives System würde die Daten "just-in-time" verarbeiten, also Änderungen laufend einarbeiten.
 
Ein wichtiger Aspekt der reaktiven Programmierung ist, dass die Verarbeitung asynchron passiert.
 
Asynchrones vs. synchrones Arbeiten oder Telefon vs. E-Mail
Ein grosser Teil der Software arbeitet heute synchron: der Programmierer spezifiziert Schritt für Schritt, was der Computer ausführen soll. Muss in einem Schritt auf etwas wie eine Datei oder eine Server-Antwort gewartet werden, blockiert das Programm bis der gewünschte Input ankommt. Dies ist ein einfaches Modell. Für Menschen ist es natürlich, synchron zu denken – eines nach dem anderen. Übertragen auf die menschliche Kommunikation ist die synchrone Kommunikation wie bei einem Telefongespräch: Ich bin am Telefon und warte auf die Antwort meines Gesprächspartners.
 
Dem entgegengesetzt steht die asynchrone Kommunikationsweise der E-Mail: ich schicke die E-Mail ab und erledige dann andere Arbeiten. Sobald mein E-Mail-Empfänger antwortet, habe ich eine neue E-Mail in meiner Inbox. Ich werde informiert, dass die Mail eingetroffen ist, muss aber nicht sofort reagieren und meine Arbeit unterbrechen. In einem Server könnte dies wie folgt aussehen: jedes Mal, wenn ich etwas mache, wo ich auf jemanden anderen warten müsste, schicke ich den Auftrag ähnlich einer E-Mail und fahre mit meiner Arbeit
fort (siehe Bild oben).
 
Der Vorteil asynchroner Verarbeitung – und wann sie sich lohnt
Asynchrone Verarbeitung kann effizienter sein. Es gibt heute zwei sehr weit verbreitete Webserver: Apache Httpd und Nginx. Der asynchrone und neuere Nginx ersetzt für grosse Websites mehr und mehr den synchronen Apache Httpd. Nginx braucht signifikant weniger Systemressourcen pro Netzwerkverbindung. Der Nachteil von Nginx: seine höhere Komplexität macht es schwieriger, Erweiterungen dafür zu schreiben. Der Entwickler muss sehr sorgfältig sein, um effizienten und korrekten Code zu schreiben und richtig mit Nginx zu interagieren.
 
Vereinfacht kann man also sagen: Programme für die synchrone Verarbeitung sind einfacher, asynchrone Programme dagegen effizienter. Bleibt die Frage: Wann lohnt sich die höhere Komplexität der Asynchronität? Zum Beispiel, wenn man für eine Aufgabe primär auf andere warten muss (z.B. auf das Filesystem oder einen Server) oder wenn man primär Aufgaben für andere dispatchen und koordinieren muss. Die Aufgabe muss auch genügend häufig ausgeführt werden, dass sich die höhere Komplexität für die gewonnene Performanz lohnt. Also nimmt man typisch als Webserver den asynchronen Nginx anstatt Apache, und versucht ganz allgemein Infrastruktur-Komponenten – wo Effizienz sehr wichtig ist – wie API-Gateways oder Proxies asynchron auszulegen.
 
Weitere Indikatoren für eine asynchrone Programmierung sind etwa die Tatsache, dass die heutigen Web-Browser nur asynchrone Programmierung unterstützen. Auch gut gemachte Benutzeroberflächen sollten nicht blockieren (also asynchron sein), nur weil eine Harddisk oder ein Server gerade langsam reagieren.
 
Programmierabstraktionen oder: wie den Algorithmus formulieren?
Es gibt also doch gute Gründe, in gewissen Situationen asynchron zu programmieren. Weil asynchron zu programmieren nicht trivial ist, werden aktuell ganz verschiedene Innovationen ausprobiert, um die mit der Asynchronität zusammenhängende Komplexität zu reduzieren: Es geht darum, mit welchen Basis-Konstrukten ich meine Algorithmen beschreiben kann.
 
Man kann die reaktive Programmierung auch als solche Innovation sehen, um asynchrone Programme sauberer zu schreiben. Ihr Basis-Ausdruck sind die Daten-Streams. Als Namen für solche Streams scheint sich Observables einzubürgern. Ich kann über solche Streams Quellen von Signalen verbinden mit Transformatoren, welche die Streams modifizieren und solche, welche auf die Daten reagieren. Ich definiere also eine Pipeline von Operationen, eine nach der anderen, um meine Daten zu verarbeiten.
 
Die Pipeline stellt auch sicher, dass zu viele ankommende Ereignisse nicht den Stream auffüllen – es wird also intern die Geschwindigkeit des Datenstroms koordiniert. Werden zu viele Ereignisse eingespielt, erhält die einspielende Seite eine Rückmeldung, dass sie bremsen soll. Dazu wird sie notfalls eventuell sogar unwichtigere Ereignisse weglassen – je nach Anforderung.
 
Spannend ist, dass diese Innovation für GUIs ebenso wie für Server-Anwendungen Einsatz findet. Es gibt eine ganze Sammlung an Implementierungen für alle gängigen Programmiersprachen.
 
Ein weiteres Beispiel für den Einsatz der reaktiven Programmierung ist die Videoplattform Netflix: Dort gibt es eine Überwachungsanwendung für die sehr vielen Server und Anwendungen, welche laufend Messwerte via Datenströme sammelt und daraus Auswertungen macht. Auf welche Messwerte die Anwendung zurückgreift und dann auswertet, kann jeweils dynamisch angepasst werden.
 
Was heisst dies in der Praxis?
Die reaktive Programmierung erfordert ein anderes Denken und Erfahrung – es ist weiter weg von unserem normalerweise synchronen Denken und damit komplexer. Diese grössere Komplexität zieht sich durch den Entwurf der Schritte der reaktiven Pipeline, das Testen und das Verstehen eines reaktiven Systems.
 
Für viele Anwendungsfälle wird sich nichts ändern und man wird weiterhin wie schon heute Software entwickeln. In Fällen, in denen Anwendungen auf Datenströme reagieren müssen und man auf eine höhere Effizienz oder schnelle Reaktionszeit angewiesen ist, lohnt es sich aber, die Konzepte der reaktiven Programmierung zu evaluieren. (Philipp Oser)
 
Philipp Oser ist Lead Architekt bei Elca Informatik.