Event- und Thread-basierter Ansatz zur asynchronen Verarbeitung von Daten mit PHP
Asynchronous I/O, oder doch Threads?
Tim Wagner
In den letzten Monaten erobern, angelehnt an Node.js, zunehmend Event-driven Non-blocking-Lösungen für die ansynchrone Verarbeitung von Daten ihren Platz im PHP-Ökosystem. Neben dem auch als Asynchronous I/O bezeichneten Programmierkonzept haben mittlerweile auch Threads Einzug in PHP gefunden. So entwickelt Joe Watkins seit ca. sechs Monaten an einer PHP-Extension, die Threads in PHP zur Verfügung stellt. Dieser Artikel soll Vor- und Nachteile beider Ansätze sowie denkbare Einsatzmöglichkeiten aufzeigen.
Node.js hat sich mittlerweile zu einer Art Trend entwickelt, der nach und nach auch auf die PHP-Community übergreift. So konnte der interessierte Entwickler das Interesse der Community an asynchroner Verarbeitung in PHP auf der International PHP Conference in Berlin am eigenen Leib spüren. Sowohl die Session „Asynchronous I/O in PHP“ von Thomas Weinert als auch die „Einführung in Node.js“ von Sebastian Springer waren vollkommen überlaufen.
Mit Node.js hat die JavaScript-Community endlich eine stabile und leistungsfähige Möglichkeit bekommen, die Sprache auch serverseitig einzusetzen. Für die bisher eher belächelten JavaScript-Entwickler haben sich damit völlig neue Möglichkeiten eröffnet: Plötzlich lassen sich Anwendungen komplett in JavaScript realisieren, die Unterstützung durch Entwickler serverseitiger Sprachen wie PHP ist nicht mehr notwendig. PHP-Entwickler hingegen haben mittlerweile das Problem, dass sie aufgrund der ständig wachsenden Anforderungen und Komplexität von JavaScript, HTML5 und CSS mehr und mehr auf Entwickler, die sich auf diese Bereiche spezialisiert haben, angewiesen sind. Der Spieß hat sich sozusagen umgedreht.
Node.js führt eingefleischten PHP-Entwicklern vor Augen, dass sich ihre JavaScript-Kollegen mittlerweile auf Augenhöhe befinden und die Technologie sowie die damit verbundenen Möglichkeiten PHP z. T. vielleicht sogar überholt haben. So haben sicherlich die wenigsten PHP-Entwickler jemals versucht, einen HTTP-Server auf Basis von PHP zu entwickeln, auch wenn dieser Ansatz zweifellos Vorteile mit sich bringen würde. Node.js zeigt, dass es anscheinend nicht nur Einsatzmöglichkeiten für derartige Lösungsansätze gibt, sondern Entwickler vielmehr danach verlangen, auch derartige Lösungen selbst entwickeln, erweitern und anpassen zu können.
Nach dem Vorbild von Node.js haben sich mittlerweile einige Projekte in der Community etabliert, die dem „großen“ JavaScript-Bruder nacheifern und ähnliche Ansätze auch im PHP-Umfeld etablieren wollen. Neben dem bereits zuvor angesprochenen Carica Io [1] von Thomas Weinert gehören phpDaemon [2] und ReactPHP [3] zu den wohl renommiertesten Lösungen. ReactPHP wird mittlerweile von einigen Projekten sogar im Livebetrieb eingesetzt, was die Themen sinnhaftig und Machbarkeit obsolet erscheinen lässt.
Event- vs. Thread-basierte Verarbeitung
Für die asynchrone Verarbeitung von Daten gibt es aktuell zwei favorisierte Möglichkeiten. Node.js setzt hierbei auf einen Event-driven Ansatz. Dabei werden Daten über ein Non-blocking I/O-Model angenommen und in einem Event-Loop asynchron verarbeitet. Beim Aufruf des Event-Loops wird eine Callback-Funktion angegeben, die, sobald sie die asynchrone Verarbeitung beendet hat, das Ergebnis an den aufrufenden Prozess zurückgibt. Dieser kann dann das Ergebnis, das z. B. HTML-Code sein kann, über das I/O-Modell, z. B. einen Socket, wieder an den Client zurückgeben. Da der Event-Loop als Single Thread läuft, ist die eigentliche Verarbeitung der eingehenden Anfragen immer synchron, sobald jedoch ein Event getriggert wird, können weitere Anfragen entgegengenommen und ebenfalls asynchron verarbeitet werden.
Entgegen dem zuvor beschriebenen Ansatz haben PHP-Entwickler mittlerweile auch die Möglichkeit, Threads für die asynchrone Verarbeitung von Daten einzusetzen. Hierbei wird die in C geschriebene PHP-Extension pthreads [4], die das systeminterne POSIX-Threads-API kapselt, verwendet. Die eigentliche Ausführung und die Kontrolle der einzelnen Threads liegen auch hier beim jeweiligen Betriebssystem.
Was benötige ich, um asynchron programmieren zu können?
Im Gegensatz zum Event-driven Ansatz muss PHP für die Verwendung von Threads zuerst kompiliert werden. Das bedeutet leider, dass derzeit nach unserem Kenntnisstand bei keinem der bekannteren Hoster die Verwendung von Threads möglich ist. Seit einigen Monaten existiert jedoch das Projekt appserver [5], das, ähnlich wie XAMP, eine fertig kompilierte Runtime für Mac OS X und Debian zur Verfügung stellt. Die Runtime für Mac OS X (aktuell nur Mountain Lion) kann bequem als .pkg-Paket heruntergeladen und installiert werden. Das Debian-Paket lässt sich über den Paketmanager oder das Debian-Repository deb.appserver.io installieren. Die Abhängigkeiten werden hierbei automatisch nachinstalliert. In beiden Fällen findet man die Runtime anschließend im Verzeichnis /opt/appserver.
Die Runtime stellt eine für den Einsatz von Threads und Asynchronous I/O optimierte Laufzeitumgebung bereit. Neben einer thread-save kompilierten Version von PHP 5.4.17 bringt die Runtime eine aktuelle Version von nginx, memcached sowie die PECL-Libraries pthreads und libev in der jeweils aktuellsten Version mit. Zusätzlich werden einige andere PECL-Pakete mit ausgeliefert, die jedoch über die entsprechenden php.ini-Einträge aus Performancegründen deaktiviert wurden.
Wie funktioniert ein Thread?
Da die Verwendung von Threads für PHP-Entwickler, im Gegensatz zu Entwicklern, die mit anderen Programmiersprachen wie Java arbeiten, eher Neuland sein dürfte, möchte ich im ersten Schritt kurz zeigen, wie sie eingesetzt werden können. Listing 1 zeigt die Erstellung einer neuen Klasse AThread, die die Methode run() implementiert. Diese Methode wird automatisch aufgerufen, wenn man auf das Objekt die start()-Methode aufruft. Alles, was in der run()-Methode implementiert oder aufgerufen wird, läuft in einem eigenen Thread, also sozusagen asynchron ab.
Listing 1
class AThread extends Thread {
protected $counter = 0;
public function run() {
for ($i = 0; $i < 100; $i++) {
$this->counter++;
}
echo "Counter is: “ . $this->counter . PHP_EOL;
}
}
$someThread = new AThread();
$someThread->start();
echo “Finished script” . PHP_EOL;
Dass die Ausführung auch tatsächlich asynchron erfolgt, zeigt die Ausgabe. Das Script läuft durch, gibt „Finished script“ aus und wird beendet, bevor in der run()-Methode des Threads die Klassenvariable $counter auf 100 hochgezählt wurde. Da dies in einem eigenen Thread stattfindet, läuft dieser weiter und gibt, obwohl das eigentliche Skript bereits beendet war, anschließend den String „Counter is: 100“ aus.
Wann kann ich einen Event-Loop einsetzen?
Für unseren Vergleich verwenden wir libev, da ich persönlich das objektorientierte Interface gegenüber dem prozeduralen Ansatz von libevent vorziehe. Im Gegensatz zur Verwendung von Threads ist es beim Einsatz der libev nicht erforderlich, PHP thread-save zu kompilieren. Für die Installation kann das PECL-Modul ev entweder selbst kompiliert oder ebenfalls die mit der Runtime ausgelieferte Version verwendet werden.
Beim Einsatz von libev wird die Asynchronität, wie bereits zuvor beschrieben, durch einen Event-Loop erreicht, der den Aufruf des asynchron auszuführenden Codes auslöst, sobald ein Event auf einen Dateizeiger oder ein Signal getriggert bzw. ein Timeout erreicht wird.
Listing 2 zeigt einen Event-Loop mit zwei Timer-Events, die einen global verfügbaren Counter hoch zählen. Der erste Timer wird jede Sekunde aufgerufen, solange der Counter kleiner fünf ist. Der Zweite nur einmal und zwar nach zwei Sekunden. An der Ausgabe kann man sehr gut erkennen, dass der zweite Timer den globalen Counter nach exakt zwei Sekunden um eins erhöht und anschließend der erste Timer diesen schließlich bis fünf hoch zählt.
Listing 2
$counter = 0;
$periodic = new EvPeriodic(0, 1, null, function() use(&$counter) {
$counter++;
if ($counter == 5) {
Ev::stop();
}
echo "Periodic timer counts: $counter" . PHP_EOL;
});
$timer = new EvTimer(2, 0, function() use(&$counter) {
$counter++;
echo "Timer counts: $counter" . PHP_EOL;
});
vEv::run();
Wie sieht es mit der Performance aus?
Eines der häufigsten Beispiele, die man zum Thema asynchrone Datenverarbeitung findet, ist überraschenderweise ein HTTP-Server. Es scheint so, dass viele Leute planen, einen eigenen solchen Server zu bauen. Da man einen HTTP-Server auf Basis von Threads oder auch libev bauen kann, bietet er sich für einen Performancevergleich an.
Die folgenden Beispiele sind möglichst einfach gehalten und stellen eine sehr rudimentäre, jedoch funktionierende Implementierung ohne Anspruch auf Vollständigkeit dar. Sie dienen lediglich der Durchführung eines einfachen Lasttests. Mit beiden Ansätzen lässt sich mit relativ wenig Code und Aufwand ein funktionierendes und belastbares Beispiel implementieren....