Timer, Streams und Promises verstehen
Der ReactPHP Event Loop ist das Herzstück jeder asynchronen PHP Anwendung mit React: ein einzelner Thread, der Timer verwaltet, Dateideskriptoren überwacht und Callbacks in einer klaren Reihenfolge ausführt. Wer den Tick Zyklus, die Timer Warteschlange und das Zusammenspiel mit Streams und Promises versteht, schreibt Code, der wirklich nicht blockiert, statt nur so auszusehen.
Inhaltsverzeichnis
- 1. Warum der ReactPHP Event Loop existiert
- 2. Der Tick Zyklus: was in jeder Runde passiert
- 3. Timer: addTimer und addPeriodicTimer im Detail
- 4. Streams: Lesen und Schreiben ohne zu blockieren
- 5. Promises: Ergebnisse asynchroner Operationen verwalten
- 6. Loop Backends: StreamSelect, Ev und Uv im Vergleich
- 7. Die größte Gefahr: blockierender Code im Loop
- 8. Signale und sauberes Herunterfahren
- 9. Event Loop Konzepte im direkten Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum der ReactPHP Event Loop existiert
PHP wurde ursprünglich für ein Request Response Modell entworfen: ein Prozess, eine Anfrage, ein Ende. Der ReactPHP Event Loop bricht mit diesem Modell, ohne die Sprache selbst zu verändern. Er ist eine Bibliothek, die einen einzigen PHP Prozess dauerhaft am Laufen hält und in einer Schleife wiederholt prüft, welche Timer abgelaufen sind und welche Dateideskriptoren lese oder schreibbereit sind. Statt für jede Verbindung einen eigenen Prozess oder Thread zu starten, verarbeitet ein einziger ReactPHP Event Loop tausende gleichzeitige Verbindungen kooperativ.
Der entscheidende Unterschied zu klassischem PHP: Es gibt keinen Kernel, der den Code präemptiv unterbricht. Jede Aufgabe muss von selbst die Kontrolle an den ReactPHP Event Loop zurückgeben, meist indem sie eine Operation registriert und sofort zurückkehrt, statt auf das Ergebnis zu warten. Dieses kooperative Modell ist der Grund, warum ein einzelner PHP Prozess mit ReactPHP mehr gleichzeitige Netzwerkverbindungen bewältigen kann als ein klassischer PHP FPM Pool mit derselben Anzahl an Worker Prozessen, solange die Arbeit selbst überwiegend I/O gebunden ist und nicht CPU gebunden.
Wichtig ist die Abgrenzung: Der ReactPHP Event Loop ersetzt keine CPU intensive Berechnung durch Magie. Er verschiebt lediglich die Wartezeit auf Netzwerk, Festplatte und Timer in eine Struktur, die während dieser Wartezeit andere Arbeit erledigen kann. Genau dieses Prinzip erklärt jede Designentscheidung, die im Rest dieses Artikels folgt, von der Timer Warteschlange bis zur Stream Verarbeitung.
2. Der Tick Zyklus: was in jeder Runde passiert
Der ReactPHP Event Loop arbeitet in Runden, die man als Ticks bezeichnet. In jedem Tick durchläuft die Loop Implementierung eine feste Reihenfolge: zuerst werden alle Timer geprüft, deren Ablaufzeit erreicht wurde, dann werden alle registrierten Streams auf Lese und Schreibbereitschaft geprüft, und schließlich werden Signale verarbeitet, falls welche registriert wurden. Diese Reihenfolge ist deterministisch, was Debugging erheblich erleichtert, weil das Verhalten bei gleichen Eingaben reproduzierbar bleibt.
Intern berechnet der ReactPHP Event Loop vor jedem Tick ein Timeout: die Zeit bis zum nächsten fälligen Timer. Dieses Timeout wird an die zugrunde liegende Systemfunktion übergeben, meist stream_select, ev_run oder uv_run, je nach installiertem Backend. Sind keine Streams aktiv, aber ein Timer in der Zukunft registriert, blockiert der Prozess exakt so lange, bis der Timer fällig wird oder ein Stream Ereignis eintrifft, je nachdem was zuerst passiert. Das minimiert unnötige CPU Last, ohne Reaktionszeit zu verlieren.
Ein Tick endet erst, wenn alle in dieser Runde fälligen Callbacks abgearbeitet sind. Registriert ein Callback während seiner Ausführung einen neuen Timer oder Stream, wird dieser erst im nächsten Tick berücksichtigt, niemals im selben. Diese Regel verhindert Endlosschleifen innerhalb eines einzigen Ticks und ist ein zentraler Unterschied zu naiven, selbstgeschriebenen Polling Schleifen, die diese Garantie ohne explizite Implementierung nicht bieten.
<?php
declare(strict_types=1);
use React\EventLoop\Loop;
// Get the global event loop instance (react/event-loop 1.3+)
$loop = Loop::get();
$loop->addTimer(1.0, function (): void {
echo "Tick after 1 second\n";
});
$loop->addPeriodicTimer(0.5, function (): void {
echo "Periodic tick every 500ms\n";
});
echo "Loop starts now, script does not block here\n";
// The loop keeps the process alive until explicitly stopped
$loop->run();
echo "This line only runs after $loop->stop() was called\n";
3. Timer: addTimer und addPeriodicTimer im Detail
Der ReactPHP Event Loop bietet zwei grundlegende Timer Methoden. addTimer registriert einen einmaligen Callback, der nach einer angegebenen Anzahl Sekunden ausgeführt wird, wobei Bruchteile von Sekunden erlaubt sind. addPeriodicTimer registriert einen Callback, der in regelmäßigen Abständen wiederholt aufgerufen wird, bis er explizit über cancelTimer entfernt wird. Beide Methoden geben ein Timer Objekt zurück, das für die spätere Entfernung benötigt wird, ein Detail, das leicht übersehen wird und dann zu Timern führt, die sich nie mehr stoppen lassen.
Intern verwaltet der ReactPHP Event Loop Timer nicht als einfache Liste, sondern als Prioritätswarteschlange, sortiert nach Ablaufzeitpunkt. Das erlaubt es der Loop Implementierung, mit konstantem Aufwand den nächsten fälligen Timer zu finden, statt bei jedem Tick alle registrierten Timer durchzugehen. Bei tausenden gleichzeitig aktiven Timern, etwa für Verbindungs Timeouts in einem Server mit vielen Clients, macht dieser Unterschied den messbaren Unterschied zwischen einer Anwendung, die linear mit der Verbindungsanzahl skaliert, und einer, die quadratisch skaliert.
Eine häufige Falle: Ein periodischer Timer, dessen Callback selbst länger läuft als das Intervall, häuft im ReactPHP Event Loop keine Aufrufe an. Der nächste Aufruf erfolgt erst, wenn der aktuelle Callback vollständig zurückgekehrt ist, und dann relativ zur tatsächlichen Ausführungszeit, nicht zur ursprünglich geplanten Zeit. Wer präzise Taktung benötigt, etwa für Metriken Exporte, sollte die tatsächliche Zeit selbst messen, statt sich blind auf die Intervallangabe zu verlassen.
4. Streams: Lesen und Schreiben ohne zu blockieren
Netzwerk I/O ist der Hauptgrund, warum der ReactPHP Event Loop überhaupt existiert. Die Bibliothek react/stream bietet ReadableResourceStream und WritableResourceStream als Wrapper um native PHP Stream Ressourcen, die in den Non Blocking Modus versetzt werden. Statt fread aufzurufen und zu warten, registriert der ReactPHP Event Loop den zugrunde liegenden Dateideskriptor beim Betriebssystem und ruft den registrierten Callback erst auf, wenn tatsächlich Daten vorliegen.
Diese Streams emittieren Events nach dem Beobachter Muster: data für neue Daten, end wenn die Gegenseite die Verbindung sauber schließt, error bei Fehlern und close wenn die Ressource endgültig freigegeben wurde. Dieses Modell erlaubt es, komplexe Verarbeitungsketten zu bauen, ohne jemals eine Zeile blockierenden Codes zu schreiben, weil jeder Schritt in der Kette nur reagiert, wenn tatsächlich Daten verfügbar sind.
Wichtig für die Praxis: Rückstau, im Englischen Backpressure genannt, muss beim Schreiben aktiv beachtet werden. Schreibt eine Anwendung schneller in einen Stream, als die Gegenseite lesen kann, füllt sich ein interner Puffer im ReactPHP Event Loop unbegrenzt und der Speicherverbrauch wächst. Die write Methode gibt einen Boolean zurück, der signalisiert, ob weiter geschrieben werden sollte oder ob auf das drain Event gewartet werden muss, bevor weitere Daten gesendet werden.
<?php
declare(strict_types=1);
use React\EventLoop\Loop;
use React\Socket\SocketServer;
use React\Socket\ConnectionInterface;
$loop = Loop::get();
$server = new SocketServer('0.0.0.0:8090', [], $loop);
$server->on('connection', function (ConnectionInterface $connection): void {
$connection->on('data', function (string $chunk) use ($connection): void {
// Echo back with a simple protocol prefix
$response = "ECHO: " . trim($chunk) . "\n";
// Respect backpressure: write() returns false if the buffer is full
$canWriteMore = $connection->write($response);
if (!$canWriteMore) {
$connection->once('drain', function () use ($connection): void {
error_log('Write buffer drained, ready for more data');
});
}
});
$connection->on('close', function (): void {
error_log('Client disconnected');
});
});
echo "Server listening on port 8090\n";
$loop->run();
5. Promises: Ergebnisse asynchroner Operationen verwalten
Sobald mehrere asynchrone Operationen im ReactPHP Event Loop voneinander abhängen, wird reines Callback verketten schnell unübersichtlich, ein Problem, das in der JavaScript Welt als Callback Hölle bekannt wurde. Die Bibliothek react/promise löst das mit dem Promise Muster: eine Operation gibt sofort ein Promise Objekt zurück, das später entweder erfüllt oder verworfen wird, sobald das eigentliche Ergebnis im ReactPHP Event Loop vorliegt.
Die Methode then registriert Callbacks für Erfolg und Fehlschlag und gibt selbst wieder ein Promise zurück, was Verkettung erlaubt: $promise->then($onSuccess, $onError). Gibt der Callback in then selbst ein weiteres Promise zurück, wartet die Kette automatisch auf dessen Auflösung, bevor der nächste Schritt ausgeführt wird. Diese Verkettung bildet die Grundlage, auf der Bibliotheken wie react/http und react/mysql aufbauen, um komplexe asynchrone Workflows lesbar zu halten.
Funktionen wie React\Promise\all und React\Promise\race koordinieren mehrere Promises gleichzeitig innerhalb des ReactPHP Event Loop. all wartet, bis sämtliche übergebenen Promises erfüllt sind, und liefert ein Array aller Ergebnisse in der ursprünglichen Reihenfolge. race löst sich auf, sobald das erste Promise fertig ist, was sich für Timeout Muster eignet: ein Promise für die eigentliche Operation gegen ein Promise für einen Timer antreten lassen und das schnellere gewinnen lassen.
<?php
declare(strict_types=1);
use React\EventLoop\Loop;
use React\Promise\Promise;
use function React\Promise\all;
$loop = Loop::get();
function fetchUserAsync(int $id): Promise
{
global $loop;
return new Promise(function (callable $resolve, callable $reject) use ($id, $loop): void {
// Simulated async database call using a timer
$loop->addTimer(0.2, function () use ($resolve, $id): void {
$resolve(['id' => $id, 'name' => "User {$id}"]);
});
});
}
$promises = [
fetchUserAsync(1),
fetchUserAsync(2),
fetchUserAsync(3),
];
// Wait for all three fake database calls in parallel, not sequentially
all($promises)->then(function (array $users): void {
foreach ($users as $user) {
echo "Loaded: {$user['name']}\n";
}
});
$loop->run();
6. Loop Backends: StreamSelect, Ev und Uv im Vergleich
Der ReactPHP Event Loop ist eine Abstraktion über mehrere austauschbare Implementierungen, sogenannte Loop Backends. Ist keine PHP Erweiterung installiert, greift die Bibliothek auf StreamSelectLoop zurück, das intern die native Funktion stream_select nutzt. Diese Implementierung funktioniert überall, wo PHP läuft, hat aber eine praktische Obergrenze: stream_select kann auf vielen Systemen standardmäßig nicht mehr als 1024 Dateideskriptoren gleichzeitig überwachen.
Ist die PHP Erweiterung ext-ev oder ext-uv installiert, wählt der ReactPHP Event Loop automatisch ExtEvLoop beziehungsweise ExtUvLoop. Diese Implementierungen binden an die C Bibliotheken libev beziehungsweise libuv, die intern effizientere Systemaufrufe wie epoll unter Linux oder kqueue unter BSD und macOS verwenden. Das Ergebnis: deutlich mehr gleichzeitige Verbindungen bei gleichzeitig geringerer CPU Last, weil die Überwachung nicht mehr linear mit der Anzahl der Deskriptoren skaliert.
Für die meisten Projekte reicht StreamSelectLoop vollkommen aus, insbesondere wenn die Anzahl gleichzeitiger Verbindungen im niedrigen Tausenderbereich bleibt. Erst bei Servern, die zehntausende gleichzeitige Langzeitverbindungen halten müssen, etwa für WebSocket Gateways oder Chat Systeme, zahlt sich die Installation von ext-ev spürbar aus. Der ReactPHP Event Loop API bleibt dabei identisch, ein Wechsel des Backends erfordert keine Codeänderung, nur die Installation der passenden PHP Erweiterung.
7. Die größte Gefahr: blockierender Code im Loop
Der ReactPHP Event Loop läuft in einem einzigen Thread. Jeder Callback, der länger als Millisekunden braucht, blockiert währenddessen sämtliche anderen Timer und Streams, weil der Loop erst nach dessen vollständigem Abschluss weiterlaufen kann. Ein einziger synchroner file_get_contents Aufruf auf eine langsame externe URL oder eine unoptimierte SQL Abfrage über den klassischen PDO Treiber kann so den gesamten Server für alle gleichzeitigen Clients einfrieren lassen.
Das ist der wichtigste mentale Unterschied zwischen klassischem PHP und dem Betrieb im ReactPHP Event Loop: In klassischem PHP FPM betrifft ein langsamer Request nur diesen einen Worker Prozess, andere Anfragen laufen unbeeinflusst in parallelen Prozessen weiter. Im Event Loop Modell betrifft ein blockierender Aufruf sofort alle gleichzeitig verwalteten Verbindungen, weil es keine Prozessgrenze gibt, die die Auswirkung eindämmt.
Die Lösung liegt darin, konsequent asynchrone Gegenstücke zu klassischen Funktionen zu verwenden: react/mysql statt PDO, react/http Client statt cURL, react/filesystem statt file_get_contents. Für seltene Fälle, in denen kein asynchrones Gegenstück existiert, bietet sich an, die blockierende Arbeit in einen separaten Kindprozess auszulagern, etwa über react/child-process, damit der ReactPHP Event Loop selbst reaktionsfähig bleibt, während der Kindprozess die schwere Arbeit erledigt.
8. Signale und sauberes Herunterfahren
Ein lang laufender Prozess, der den ReactPHP Event Loop hostet, muss auf Betriebssystemsignale reagieren können, insbesondere wenn er unter Supervisor, systemd oder in einem Docker Container verwaltet wird. Die Methode addSignal registriert einen Callback für Signale wie SIGTERM oder SIGINT, ohne dass die PHP pcntl Erweiterung manuell in Ticks eingebunden werden muss, der ReactPHP Event Loop übernimmt diese Integration selbst.
Ein sauberes Herunterfahren bedeutet in der Praxis: Beim Empfang von SIGTERM werden neue eingehende Verbindungen sofort abgelehnt, bestehende Verbindungen erhalten eine kurze Frist zur Fertigstellung ihrer aktuellen Anfrage, und erst danach wird $loop->stop() aufgerufen, um den ReactPHP Event Loop zu beenden. Ohne diese Sequenz reißt ein hartes Beenden aktive Verbindungen ab und produziert Fehler auf Client Seite, die bei einem geordneten Shutdown vermeidbar wären.
In containerisierten Umgebungen ist die Frist bis zum harten Kill meist begrenzt, häufig auf zehn Sekunden bei Kubernetes und Docker Compose Standardwerten. Der Shutdown Callback im ReactPHP Event Loop sollte diese Frist kennen und notfalls nach Ablauf selbst hart beenden, statt darauf zu vertrauen, dass die Orchestrierungsschicht unbegrenzt wartet.
<?php
declare(strict_types=1);
use React\EventLoop\Loop;
$loop = Loop::get();
$activeConnections = [];
$shuttingDown = false;
function gracefulShutdown(&$shuttingDown, array &$activeConnections, $loop): void
{
$shuttingDown = true;
error_log('SIGTERM received, refusing new connections');
// Give active connections 5 seconds to finish, then force stop
$loop->addTimer(5.0, function () use ($loop): void {
error_log('Grace period expired, stopping loop now');
$loop->stop();
});
}
$loop->addSignal(SIGTERM, function () use (&$shuttingDown, &$activeConnections, $loop): void {
gracefulShutdown($shuttingDown, $activeConnections, $loop);
});
$loop->addSignal(SIGINT, function () use ($loop): void {
error_log('SIGINT received, stopping immediately');
$loop->stop();
});
echo "Server running, press Ctrl+C to stop\n";
$loop->run();
9. Event Loop Konzepte im direkten Vergleich
Der ReactPHP Event Loop bietet für viele Aufgaben mehrere mögliche Werkzeuge an. Welches davon geeignet ist, hängt vom konkreten Anwendungsfall ab, von der erwarteten Anzahl gleichzeitiger Operationen bis zur benötigten Präzision der Zeitsteuerung.
| Aufgabe | Ungeeignet im Loop | Empfohlenes Werkzeug | Grund |
|---|---|---|---|
| Einmaliges Timeout | sleep(1) |
addTimer(1.0, …) |
Blockiert den Loop nicht |
| HTTP Anfrage | curl_exec() |
react/http Browser |
Non Blocking, Promise basiert |
| Datenbankzugriff | PDO::query() |
react/mysql |
Wartet nicht synchron auf das Netzwerk |
| Mehrere Ergebnisse abwarten | verschachtelte Callbacks | React\Promise\all() |
Lesbare, flache Struktur |
| Zehntausende Verbindungen | StreamSelectLoop |
ExtEvLoop / ExtUvLoop |
epoll/kqueue statt select() |
Die Tabelle zeigt ein durchgängiges Muster: Jede Zeile ersetzt einen synchronen, blockierenden Aufruf durch ein asynchrones Gegenstück, das dem ReactPHP Event Loop die Kontrolle sofort zurückgibt. Wer dieses Muster konsequent auf den gesamten Anwendungscode anwendet, erhält einen Server, der mit einem einzigen Prozess Lasten bewältigt, für die klassisches PHP viele parallele Worker Prozesse benötigen würde.
Mironsoft
Asynchrone PHP Architekturen und ReactPHP Beratung
Server, der wirklich gleichzeitig arbeitet, statt nur so zu wirken?
Wir analysieren bestehende PHP Anwendungen auf blockierende Stellen, entwerfen ReactPHP basierte Event Loop Architekturen und begleiten die Migration von klassischem PHP FPM zu langlaufenden, asynchronen Diensten.
Architektur Review
Blockierende Aufrufe im bestehenden Code identifizieren und priorisieren
ReactPHP Implementierung
Server, Worker und Clients auf Event Loop Basis konzipieren und umsetzen
Betrieb & Monitoring
Graceful Shutdown, Signal Handling und Loop Backend Auswahl für Produktion
10. Zusammenfassung
Der ReactPHP Event Loop löst ein grundlegendes Problem klassischer PHP Anwendungen: die Unmöglichkeit, in einem einzigen Prozess auf viele gleichzeitige, langsame I/O Operationen effizient zu warten. Der Tick Zyklus mit fester Reihenfolge aus Timer Prüfung, Stream Polling und Signalverarbeitung macht das Verhalten vorhersagbar. Timer über addTimer und addPeriodicTimer, Streams über react/stream und Koordination über Promises bilden zusammen das Fundament, auf dem praktisch jede ReactPHP Anwendung aufbaut.
Die größte Herausforderung bleibt Disziplin: jeder blockierende Aufruf im ReactPHP Event Loop friert die gesamte Anwendung ein, weil es keine Prozessgrenze gibt, die den Schaden begrenzt. Wer konsequent auf asynchrone Bibliotheken setzt, Backpressure beim Schreiben beachtet und Signale für ein sauberes Herunterfahren registriert, erhält einen Server, der mit minimalen Ressourcen erstaunlich viele gleichzeitige Verbindungen bedient, weit über das hinaus, was ein klassischer PHP FPM Pool mit vergleichbarer Hardware leisten würde.
Der ReactPHP Event Loop im Detail — Das Wichtigste auf einen Blick
Tick Zyklus
Feste Reihenfolge pro Runde: Timer prüfen, Streams pollen, Signale verarbeiten. Deterministisch und reproduzierbar.
Timer & Streams
addTimer/addPeriodicTimer für Zeitsteuerung, react/stream für Non Blocking I/O mit Backpressure Handling.
Promises
then, all und race koordinieren mehrere asynchrone Ergebnisse ohne verschachtelte Callbacks.
Backends & Blockierung
StreamSelect für die meisten Fälle, Ev/Uv für hohe Skalierung. Nie blockierende Aufrufe im Loop Thread ausführen.