Async PHP ist da: Ein Praxis-Guide für Fibers

Async PHP ist da: Ein Praxis-Guide für Fibers

Erinnerst du dich an die Callback-Hölle? An das endlose Verketten von .then()-Aufrufen? Zeit, das alles zu vergessen. PHP Fibers sind hier und sie haben alles verändert.

Einleitung: Die alte Welt der PHP Concurrency

Lange Zeit war der Ansatz von PHP für I/O ziemlich simpel: Du fragst was an (eine Datenbankabfrage, ein API-Call) und dann wartest du. Dein gesamter Prozess saß einfach nur rum, blockiert, und hat Däumchen gedreht, bis die Antwort kam. Das war einfach, aber furchtbar ineffizient.

Wir haben versucht, es zu fixen. Wir haben Callbacks benutzt, was zur berüchtigten “Callback-Hölle” führte. Dann kamen Promises (danke an ReactPHP und Amp!), die eine riesige Verbesserung waren, aber immer noch eine andere Denkweise erforderten, mit .then()-Ketten und einer Fehlerbehandlung, die nicht gerade intuitiv war.

Dann kam PHP 8.1 und damit die Fibers. Fibers sind ein Low-Level-Primitiv für kooperatives Multitasking. Einfach gesagt: Sie lassen uns asynchronen Code schreiben, der aussieht und sich anfühlt wie synchroner Code. Lass uns da mal eintauchen.

Was genau ist ein Fiber?

Ein Fiber ist im Grunde eine Funktion, die pausiert und wieder fortgesetzt werden kann.

Stell es dir so vor: Wenn du eine normale Funktion aufrufst, läuft sie, bis sie einen Wert zurückgibt. Sie kann nicht unterbrochen werden. Ein Fiber hingegen kann gestartet werden, läuft ein bisschen und sagt dann: “Okay, ich warte hier auf was, ich pausiere mich mal kurz.” Während er pausiert ist, kann anderer Code laufen. Wenn das, worauf er gewartet hat, fertig ist, kannst du den Fiber fortsetzen, und er macht genau da weiter, wo er aufgehört hat – mit seinem kompletten Stack (lokale Variablen und alles) intakt.

Der Schlüsselbegriff hier ist “kooperativ”. Ein Fiber entscheidet selbst, wann er mit Fiber::suspend() pausiert. Das ist anders als bei präemptivem Multitasking (wie bei Threads), wo das Betriebssystem Dinge unterbrechen kann, wann immer es will.

Der Haken: Fibers brauchen einen Manager

Ein Fiber allein ist nur eine pausierbare Funktion. Das ist mächtig, aber er weiß nicht, wann er sich fortsetzen soll. Um Fibers für I/O nützlich zu machen, brauchst du zwei weitere Dinge:

  1. Einen Event-Loop: Eine Schleife, die ständig auf I/O-Ereignisse prüft (wie “dieser Socket ist jetzt lesbar”).
  2. Einen Scheduler: Code, der entscheidet, welcher Fiber als Nächstes ausgeführt wird, und einen pausierten Fiber wieder aufweckt, wenn sein I/O-Ereignis eintritt.

Du könntest das selbst schreiben, aber das solltest du wirklich nicht. Hier kommen Libraries wie Amp und Revolt ins Spiel. Sie bieten robuste, produktionsreife Event-Loops, die mittlerweile auf Fibers aufbauen.

Ein praktisches Beispiel: Gleichzeitige HTTP-Requests

Schauen wir uns an, wie das in der Praxis aussieht. Wir wollen Daten von zwei verschiedenen APIs abrufen und nicht warten, bis die erste fertig ist, bevor wir die zweite starten.

Wir benutzen amphp/http-client, einen mächtigen Async-Client, der unter der Haube Fibers verwendet.

Zuerst musst du die nötigen Pakete installieren:

composer require amphp/http-client revolt/event-loop

Und jetzt der Code. Das alles läuft in einem einzigen PHP-Skript.

<?php

require_once __DIR__ . '/vendor/autoload.php';

use Amp\Http\Client\HttpClientBuilder;
use Amp\Http\Client\Request;

// Diese Funktion wird vom Event-Loop in einem Fiber ausgeführt
function fetchUrl(string $url): mixed
{
    try {
        // 1. Einen neuen async HTTP-Client erstellen
        $client = HttpClientBuilder::buildDefault();

        // 2. Den Request machen. Das gibt sofort ein "Future" zurück.
        $response = $client->request(new Request($url));

        // 3. Auf das Ergebnis warten. HIER passiert die Magie.
        // Der Fiber pausiert hier und erlaubt anderen Fibers zu laufen.
        $body = $response->getBody()->buffer();

        echo "Abruf von $url beendet\n";
        return json_decode($body, true);

    } catch (\Exception $e) {
        echo "Fehler beim Abruf von $url: " . $e->getMessage() . "\n";
        return null;
    }
}

// Der Haupt-Ausführungsblock
Revolt\EventLoop::run(function () {
    $start = microtime(true);

    echo "Starte Requests...\n";

    // Erstelle zwei "Futures", indem wir unsere Funktion aufrufen.
    // Der Event-Loop wird diese so planen, dass sie nebenläufig laufen.
    $future1 = Revolt\EventLoop::Future->map(fn() => fetchUrl('https://api.github.com/users/amphp'));
    $future2 = Revolt\EventLoop::Future->map(fn() => fetchUrl('https://api.github.com/users/revoltphp'));

    // Warte, bis beide Futures abgeschlossen sind.
    // Der Event-Loop managt das Pausieren/Fortsetzen der Fibers, bis beide fertig sind.
    [$githubUser1, $githubUser2] = Revolt\EventLoop\Future\all([$future1, $future2]);

    $end = microtime(true);

    printf("Benutzer '%s' und '%s' abgerufen\n", $githubUser1['login'] ?? 'n/a', $githubUser2['login'] ?? 'n/a');
    printf("Gesamtzeit: %.2f Sekunden\n", $end - $start);
});

Was passiert hier genau?

  1. Wir definieren eine Funktion fetchUrl, die einen HTTP-Request durchführt.
  2. Innerhalb von Revolt\EventLoop::run() planen wir, diese Funktion zweimal nebenläufig auszuführen. Der Event-Loop packt jeden Aufruf in einen Fiber.
  3. Wenn $response->getBody()->buffer() aufgerufen wird, weiß der HTTP-Client, dass die Daten noch nicht da sind. Er sagt dem Event-Loop: “Hey, ich warte auf Daten auf diesem Socket” und pausiert dann den Fiber.
  4. Der Event-Loop, jetzt frei, kann am anderen Fiber arbeiten und den zweiten HTTP-Request starten. Dieser pausiert ebenfalls.
  5. Der Event-Loop wartet nun. Wenn Daten auf dem ersten Socket ankommen, setzt der Loop den ersten Fiber fort, der seine Ausführung fortsetzt. Dasselbe passiert für den zweiten.

Wenn jeder Request 1 Sekunde dauert, wird die Gesamtzeit etwa 1 Sekunde betragen, nicht 2. Das ist die Power von nicht-blockierendem I/O. Und beachte, wie sauber der Code ist – keine Callbacks, kein .then(). Er liest sich wie ein synchrones Skript.

Fibers vs. die alte Garde

  • vs. Generatoren: Generatoren waren ein cleverer Hack, um das zu simulieren, aber sie waren klobig. Fibers sind ein dediziertes Sprachfeature mit einer richtigen API und einem eigenen Stack.
  • vs. Promises: Promises sind immer noch relevant! Tatsächlich benutzen Libraries wie Amp sie oft intern. Aber Fibers erlauben es uns, APIs zu bauen, die diese Promises konsumieren, ohne dem Endbenutzer die unschöne Ketten-Syntax aufzuzwingen.
  • vs. Swoole: Swoole ist eine PHP-Extension, die echte Parallelität mit Threads und einem hochperformanten, eingebauten Event-Loop bietet. Es ist unglaublich mächtig, fügt deinem Stack aber eine zusätzliche Komplexitätsebene hinzu. Fiber-basiertes Async ist reines PHP, das auf einem Standard-FPM- oder CLI-Prozess läuft.

Fazit: Eine neue Ära für PHP

Fibers sind kein Allheilmittel. Sie bringen neue Herausforderungen beim Debugging mit sich und erfordern ein Umdenken bezüglich “unsichtbarer Nebenläufigkeit” (ein Funktionsaufruf, der synchron aussieht, könnte deinen Code pausieren).

Dennoch sind sie ein gewaltiger Sprung nach vorn. Indem sie eine Low-Level-, standardisierte Methode für kooperatives Multitasking bereitstellen, haben sie eine neue Generation von benutzerfreundlichen Async-Bibliotheken ermöglicht. Für I/O-lastige Anwendungen bedeutet das mehr Performance, weniger Warten und saubereren Code. Es ist eine gute Zeit, PHP-Entwickler zu sein.


Hast du schon mit Fibers experimentiert? Was sind deine liebsten Async-Libraries? Schreib mir auf Twitter oder LinkedIn.

Weiterführende Literatur: