Der PHP-RFC, der eine native Polling-API definiert, wurde genehmigt und in den Master-Branch der Sprache gemergt, was Entwicklern eine schnelle Methode auf Betriebssystemebene bietet, um File Descriptor zu pollen. In Kombination mit den kürzlich hinzugefügten Fibers verfügt PHP nun über eine integrierte, effiziente Grundlage für asynchronen Code – eine Lücke, die Bibliotheken lange Zeit dazu zwang, eigene Workarounds zusammenzubasteln.

Fibers und der Event-Loop, getrennt

Fibers sind ein Low-Level-Kontrollfluss-Primitiv. Sie ermöglichen es einer Funktion, ihre Ausführung an einem gewählten Punkt zu unterbrechen und später genau dort fortzusetzen, wo sie aufgehört hat. Entscheidend ist, dass ein Fiber nichts über Sockets, Timer oder andere I/O-Quellen weiß; er pausiert einfach und startet bei Bedarf neu.

Ein Event-Loop ist der Scheduler, der entscheidet, wann ein pausierter Fiber fortgesetzt werden soll. In einem typischen Async-Stack überwacht der Loop einen Satz von File Descriptoren, wartet darauf, dass diese lesbar oder beschreibbar werden, und weckt dann den entsprechenden Fiber auf.

PHP verfügte bereits über Fibers, hatte aber keinen nativen Mechanismus, um das Betriebssystem zu fragen, welche Deskriptoren bereit waren. Das Ergebnis war eine Abhängigkeit von stream_select() oder externen Erweiterungen, und jede Async-Bibliothek musste letztlich ihre eigenen Backends für Linux’ epoll, BSD’s kqueue, Windows’ IOCP usw. schreiben.

Die neue Polling-API schließt diese Lücke. Sie bietet einen dünnen Wrapper um die Polling-Funktionen des Betriebssystems (epoll unter Linux, kqueue unter BSD/macOS usw.). Die API ersetzt keine Fibers; sie verleiht dem Event-Loop lediglich die Geschwindigkeit und Skalierbarkeit, die er benötigt.

Warum die Änderung für ReactPHP, Amp und Freunde wichtig ist

ReactPHP und Amp v3 haben ihre eigenen Abstraktionsschichten über die Polling-Mechanismen des Betriebssystems aufgebaut. Diese Schichten enthalten mehrere Code-Pfade, die jeweils auf eine bestimmte Plattform optimiert sind, und sie müssen mit Kernel-Änderungen synchron gehalten werden. Mit einer nativen Polling-API können die Bibliotheken den Großteil dieser Infrastruktur weglassen und sich auf einen einzigen, vom Kern bereitgestellten Aufruf verlassen.

  • Wartung – Weniger plattformspezifische Zweige bedeuten weniger Bugs und eine geringere Angriffsfläche für Sicherheitsüberprüfungen.
  • Performance – Der native Aufruf kommuniziert direkt mit epoll/kqueue.
  • Portabilität – Code, der auf „Vanilla“-PHP läuft, erhält nun auf allen unterstützten Betriebssystemen die gleiche Basis-Performance, ohne dass optionale Erweiterungen erforderlich sind.

Swoole hingegen bleibt ein vollständiger Laufzeit-Ersatz, der seinen eigenen Event-Loop und sein eigenes Coroutine-System mitbringt. Die Polling-API beeinflusst die Kompromisse von Swoole nicht; Entwickler, die extrem niedrige Latenzzeiten oder ein benutzerdefiniertes Speichermanagement benötigen, werden es weiterhin als separate Option in Betracht ziehen.

Ein kurzer Benchmark zeigt das Ergebnis

Ein minimaler Scheduler rief mehrere URLs über Raw Sockets ab und nutzte einen einzigen Io\Poll\Context, um mehrere Fibers zu verwalten. Zwei Beobachtungen ergaben sich:

  1. Geschwindigkeit – Das Hinzufügen von mehr gleichzeitigen Anfragen erhöht nicht die gesamte verstrichene Zeit; der Batch an Anfragen wird so schnell fertig wie die langsamste einzelne Anfrage. Mit anderen Worten: Der Overhead für Nebenläufigkeit ist effektiv gleich null.
  2. CPU-Kosten – Bei stream_select() steigt die CPU-Auslastung mit zunehmender Anzahl der Streams spürbar an, da die Funktion bei jedem Aufruf über jeden Deskriptor iterieren muss. Die Kosten der Polling-API bleiben von drei bis zu dreißig Streams konstant, dank der Fähigkeit des Kernels, viele Deskriptoren in einem einzigen Systemaufruf zu überwachen.

Was Entwickler jetzt tun sollten

  • Nutzen Sie Amp v3, wenn Sie einen fiber-nativen Stil bevorzugen, der direkt mit der neuen API harmoniert. Die öffentliche Schnittstelle bildet bereits auf den zugrunde liegenden Poller ab, sodass Sie den Vorteil genießen, ohne Ihren Code umschreiben zu müssen.
  • Bleiben Sie bei ReactPHP, wenn Sie die explizite Kontrolle über den Lebenszyklus des Loops bevorzugen.
  • Vermeiden Sie eigene Scheduler für Produktions-Workloads. Schreiben Sie keinen eigenen Scheduler für den Produktivbetrieb.