Fibers in PHP: Kooperatives Multitasking von Grund auf
AI generated
<?php
8.4
PHP · PHP 8.1+ · Concurrency · Core Language
Fibers in PHP
kooperatives Multitasking von Grund auf

Fibers geben PHP-Code die Fähigkeit, Ausführung an einer beliebigen Stelle im Aufrufstapel zu unterbrechen und später exakt dort fortzusetzen, ohne die aufrufende Logik in Callbacks oder Generatoren zu zerlegen. Dieser Artikel geht die Fiber-Klasse Schritt für Schritt durch, von start und resume über Fehlerbehandlung mit throw bis zu einem eigenen kooperativen Scheduler und den tatsächlichen Kosten bei Speicher und Stack.

18 Min. Lesezeit Fiber-Klasse · suspend · resume · throw · Scheduler PHP 8.1+ · 8.4 · kein Event-Loop-Zwang

1. Einordnung: Was Fibers sind

Seit PHP 8.1 steht mit Fiber eine Sprachfunktion zur Verfügung, die eine unterbrechbare Ausführungseinheit mit eigenem Aufrufstapel darstellt. Der zentrale Unterschied zu einer normalen Funktion: Der Code innerhalb eines Fibers kann seine Ausführung an praktisch jeder Stelle anhalten und später exakt an dieser Stelle fortsetzen, inklusive aller lokalen Variablen und des aktuellen Aufrufkontexts. Damit lassen sich Programmabläufe schreiben, die sich wie normaler, sequenzieller Code lesen, obwohl sie in Wahrheit an mehreren Stellen pausieren und wieder aufgenommen werden.

Kooperatives Multitasking bedeutet dabei: Ein Fiber wechselt niemals von selbst und niemals unvorhersehbar. Der Kontrollwechsel passiert ausschließlich dann, wenn der Code innerhalb des Fibers explizit Fiber::suspend() aufruft. Das steht im Gegensatz zu präemptivem Multitasking, wie es Betriebssystem-Threads kennen, bei dem der Scheduler jederzeit unterbrechen kann, oft mitten in einer Operation, die eigentlich atomar sein sollte. Weil ein Fiber nur an selbst gewählten Punkten pausiert, entfällt die Notwendigkeit für Mutexe oder Locks rund um gemeinsam genutzten Zustand, solange dieser Zustand nicht über einen Suspend-Punkt hinweg inkonsistent gemacht wird.

Wichtig ist die Abgrenzung zu echter Parallelität: Fibers laufen weiterhin in einem einzigen PHP-Prozess und auf einem einzigen Betriebssystem-Thread. Es findet keine gleichzeitige Ausführung auf mehreren CPU-Kernen statt. Was Fibers liefern, ist Nebenläufigkeit durch Verschachtelung: Viele pausierte Aufgaben werden reihum fortgesetzt, typischerweise gesteuert von einer Event-Loop, die genau dann resume() aufruft, wenn eine zuvor angestoßene I/O-Operation abgeschlossen ist.

2. Die Fiber-Klasse im Detail

Der Konstruktor new Fiber(callable $callback) erzeugt zunächst nur das Objekt, es wird noch kein Code ausgeführt. Erst der Aufruf von start(...$args) beginnt die eigentliche Ausführung: Der übergebene Callback läuft mit den mitgegebenen Argumenten los, bis er entweder normal zurückkehrt, eine Exception wirft, oder Fiber::suspend() aufruft. In allen drei Fällen kehrt start() zum aufrufenden Code zurück. Die Instanzmethoden isStarted(), isRunning(), isSuspended() und isTerminated() spiegeln jederzeit den aktuellen Zustand eines Fibers wider und lassen sich für Diagnose und Scheduler-Logik direkt abfragen.

Nach einer Unterbrechung setzt resume(mixed $value = null) den Fiber exakt an der pausierten Stelle fort. Ist der Fiber vollständig durchgelaufen, liefert getReturn() den Rückgabewert des ursprünglichen Callbacks, allerdings nur, wenn isTerminated() bereits true ist. Ein vorzeitiger Aufruf von getReturn() löst einen FiberError aus, was in der Praxis ein zuverlässiges Signal dafür ist, dass ein Scheduler den Lebenszyklus eines Fibers nicht korrekt nachverfolgt.


declare(strict_types=1);

// Create a Fiber; the callback does not run yet
$fiber = new Fiber(function (int $start): int {
    echo "Fiber started with {$start}\n";

    // Pause execution here; control returns to the caller of start()
    $received = Fiber::suspend('halfway point');
    echo "Resumed with: {$received}\n";

    return $start * 2;
});

// start() runs the callback until the first suspend() call
$firstSignal = $fiber->start(21);
echo "Caller received: {$firstSignal}\n"; // halfway point

echo $fiber->isSuspended() ? "Fiber is suspended\n" : "";

// resume() continues execution past the suspend() call
$fiber->resume('go on');

if ($fiber->isTerminated()) {
    echo "Return value: " . $fiber->getReturn() . "\n"; // 42
}

3. Wertübergabe zwischen Fiber und Aufrufer

Die statische Methode Fiber::suspend($value) wird von innerhalb des Fibers aufgerufen und pausiert dessen Ausführung. Der Wert, der an suspend() übergeben wird, taucht auf der Aufrufer-Seite als Rückgabewert von start() oder resume() wieder auf. Umgekehrt gilt dasselbe Prinzip: Der Wert, der beim nächsten resume($value) übergeben wird, ist genau der Wert, den der pausierte Fiber::suspend()-Aufruf innerhalb des Fibers als Rückgabewert erhält. Damit entsteht ein bidirektionaler Kanal zwischen Aufrufer und Fiber, ohne dass eine zusätzliche Datenstruktur wie eine Queue oder ein Channel-Objekt notwendig wäre.

Der entscheidende Unterschied zu einem Generator: yield ist syntaktisch an die Funktion gebunden, in der es steht. Fiber::suspend() hingegen kann aus einer beliebig tief verschachtelten Funktion heraus aufgerufen werden, solange diese Funktion irgendwann von innerhalb des Fibers aufgerufen wurde. Das funktioniert, weil ein Fiber einen vollständigen eigenen Aufrufstapel besitzt und nicht nur einen einzelnen Funktionsrahmen verwaltet, wie es beim Generator-Coroutine-Modell der Fall ist.


declare(strict_types=1);

// A helper function several call frames deep inside the Fiber
function readNextChunk(int $index): string
{
    // suspend() works here even though this is not the Fiber's top-level callback
    $ready = Fiber::suspend(['status' => 'waiting', 'chunk' => $index]);
    return "chunk-{$index}-{$ready}";
}

$fiber = new Fiber(function (): array {
    $results = [];
    foreach (range(1, 3) as $index) {
        $results[] = readNextChunk($index);
    }
    return $results;
});

$signal = $fiber->start();
while (!$fiber->isTerminated()) {
    // Bidirectional exchange: caller sends "ok", Fiber uses it in its return value
    $signal = $fiber->resume('ok');
}

var_dump($fiber->getReturn());

4. Fehlerbehandlung: Exceptions in Fibers

Neben resume() bietet die Fiber-Instanz die Methode throw(Throwable $exception). Sie wirkt wie resume(), mit dem Unterschied, dass statt eines Rückgabewerts eine Exception genau an der Stelle geworfen wird, an der der Fiber pausiert war, also am wartenden Fiber::suspend()-Aufruf. Innerhalb des Fibers kann diese Exception ganz normal mit try/catch rund um den suspend()-Aufruf abgefangen werden, exakt wie bei jeder anderen Exception im PHP-Code.

Bleibt die Exception innerhalb des Fibers unbehandelt, propagiert sie durch den auslösenden start()-, resume()- oder throw()-Aufruf nach außen. Aufrufender Code muss diese Methoden daher grundsätzlich in try/catch einbetten, sobald der Fiber mit unsicheren Operationen wie Netzwerk- oder Dateizugriffen arbeitet. Terminiert ein Fiber durch eine unbehandelte Exception statt durch ein normales return, wirft ein anschließender Aufruf von getReturn() ebenfalls einen FiberError, weil kein regulärer Rückgabewert existiert.


declare(strict_types=1);

final class TimeoutException extends RuntimeException
{
}

$fiber = new Fiber(function (): string {
    try {
        // Waiting point; the caller may inject an exception here via throw()
        Fiber::suspend('waiting for response');
        return 'completed normally';
    } catch (TimeoutException $e) {
        // Exception injected from the caller is caught right at the suspend point
        return 'recovered after timeout: ' . $e->getMessage();
    }
});

$fiber->start();

try {
    $fiber->throw(new TimeoutException('upstream did not answer in time'));
} catch (Throwable $unhandled) {
    // Only reached if the Fiber itself did not catch the exception
    echo 'Propagated out of the Fiber: ' . $unhandled->getMessage();
}

echo $fiber->getReturn(); // recovered after timeout: upstream did not answer in time

5. Fibers vs. Generatoren

Ein Generator kann nur innerhalb der Funktion, die selbst ein yield enthält, pausieren. Soll eine tiefer verschachtelte Funktion ebenfalls pausieren können, muss jede einzelne Zwischenfunktion mit yield from delegieren und selbst zu einem Generator werden. In der Praxis bedeutet das: Sobald irgendwo tief in einer Bibliothek eine asynchrone Operation notwendig wird, muss die gesamte Aufrufkette bis zur äußersten Ebene auf Generatoren umgestellt werden, ein Effekt, der häufig als "function coloring" bezeichnet wird.

Fibers lösen genau dieses Problem. Nur die äußerste Stelle, die den Fiber erzeugt und über start()/resume() steuert, muss sich der Existenz des Fibers bewusst sein. Jede Funktion, die von dort aus aufgerufen wird, egal wie tief verschachtelt, kann Fiber::suspend() aufrufen, ohne selbst als Generator deklariert zu sein oder eine besondere Signatur zu benötigen, weil die Unterbrechung auf dem kompletten Aufrufstapel operiert, nicht auf einem einzelnen Funktionsrahmen.

Genau aus diesem Grund haben Bibliotheken wie amphp/amp in Version 3 und das revolt/event-loop-Paket ihre Coroutine-Implementierung vollständig von Generatoren auf Fibers umgestellt. Anwendungscode, der diese Bibliotheken nutzt, kann dadurch aussehen wie gewöhnlicher, blockierender Code, während die eigentliche Nebenläufigkeit unsichtbar über Fibers im Hintergrund abläuft.

6. Ein einfacher kooperativer Scheduler

Mit ein paar Zeilen Code lässt sich ein minimaler Scheduler bauen, der mehrere Fibers reihum abarbeitet. Das Grundprinzip: Eine Warteschlange hält alle noch nicht terminierten Fibers, der Scheduler nimmt den nächsten aus der Schlange, startet ihn oder setzt ihn fort, und legt ihn, falls er weiterhin läuft, wieder ans Ende der Warteschlange. Erst wenn ein Fiber isTerminated() meldet, verlässt er die Schlange endgültig.


declare(strict_types=1);

final class CooperativeScheduler
{
    /** @var array<int, Fiber> */
    private array $queue = [];

    public function add(Fiber $fiber): void
    {
        $this->queue[] = $fiber;
    }

    public function run(): void
    {
        while ($this->queue !== []) {
            $fiber = array_shift($this->queue);

            try {
                if (!$fiber->isStarted()) {
                    $fiber->start();
                } elseif ($fiber->isSuspended()) {
                    $fiber->resume();
                }
            } catch (Throwable $e) {
                fwrite(STDERR, "Fiber failed: {$e->getMessage()}\n");
                continue;
            }

            // Requeue the Fiber at the end if it is still running its work
            if (!$fiber->isTerminated()) {
                $this->queue[] = $fiber;
            }
        }
    }
}

$scheduler = new CooperativeScheduler();

foreach (['task-a', 'task-b', 'task-c'] as $name) {
    $scheduler->add(new Fiber(function () use ($name): void {
        for ($step = 1; $step <= 3; $step++) {
            echo "{$name}: step {$step}\n";
            Fiber::suspend();
        }
    }));
}

$scheduler->run();

Ein produktiver Scheduler geht über blindes Round-Robin hinaus: Statt jeden Fiber ungeachtet seines Zustands erneut fortzusetzen, prüft er über eine Event-Loop, welche Fibers tatsächlich bereit sind, etwa weil ein Socket lesbar wurde oder ein Timer abgelaufen ist. Das Grundmuster bleibt aber identisch: Der nächste bereite Fiber wird fortgesetzt, alle anderen bleiben unangetastet, bis ihr eigener Suspend-Punkt wieder relevant wird.

7. Anwendungsfälle für Fibers

Der prägnanteste Anwendungsfall ist asynchrones I/O, das sich für den Aufrufer wie ein blockierender Funktionsaufruf verhält. Eine Funktion wie httpGet(string $url) kann intern Fiber::suspend() aufrufen und dabei einen Callback bei der Event-Loop registrieren, der erst dann feuert, wenn die HTTP-Antwort tatsächlich vorliegt. Aus Sicht des aufrufenden Codes sieht das aus wie ein ganz gewöhnlicher, sequenzieller Funktionsaufruf, obwohl im Hintergrund während der Wartezeit andere Fibers weiterlaufen.


declare(strict_types=1);

// Simplified: an event loop that resumes a Fiber once "I/O" is ready
final class TinyEventLoop
{
    /** @var array<int, array{0: Fiber, 1: float}> */
    private array $pending = [];

    public function deferUntil(Fiber $fiber, float $readyAt): void
    {
        $this->pending[] = [$fiber, $readyAt];
    }

    public function run(): void
    {
        while ($this->pending !== []) {
            usleep(1_000);
            $now = microtime(true);

            foreach ($this->pending as $key => [$fiber, $readyAt]) {
                if ($now >= $readyAt) {
                    unset($this->pending[$key]);
                    $fiber->resume();
                }
            }
        }
    }
}

$loop = new TinyEventLoop();

function fetchAsync(TinyEventLoop $loop, string $label, float $delaySeconds): string
{
    // Looks like a blocking call, but suspends the current Fiber instead
    Fiber::suspend(fn () => $loop->deferUntil(Fiber::getCurrent(), microtime(true) + $delaySeconds));
    return "result of {$label}";
}

$fiber = new Fiber(function () use ($loop): void {
    echo fetchAsync($loop, 'user-service', 0.05) . "\n";
    echo fetchAsync($loop, 'order-service', 0.02) . "\n";
});

$fiber->start();
$loop->run();

Genau dieses Muster ist die Grundlage moderner Coroutine-Bibliotheken: Statt separater synchroner und asynchroner Varianten jeder Funktion existiert nur eine einzige Implementierung, die auf Fibers aufbaut. Das beseitigt die Notwendigkeit für eigene Async-Syntax und doppelte APIs. Typische Einsatzgebiete sind Message-Queue-Konsumenten, die viele parallele Downstream-Aufrufe absetzen, HTTP-Clients mit hoher gleichzeitiger Anfragenzahl und lang laufende Worker-Prozesse, die auf mehrere unabhängige Datenquellen warten, ohne dafür Threads oder separate Prozesse zu benötigen.

8. Speicher- und Stack-Verhalten von Fibers

Jeder Fiber erhält bei seiner Erzeugung einen eigenen, separaten Ausführungsstapel, unabhängig vom Stapel des Hauptprogramms. Das unterscheidet Fibers fundamental von Generatoren, die keinen eigenen Stapel benötigen, weil sie lediglich den Zustand eines einzelnen Funktionsrahmens zwischen den Aufrufen konservieren. Wer tausende gleichzeitig offene Fibers hält, zahlt dafür einen messbaren, realen Speicherpreis, den ein vergleichbares Generator-basiertes Design in dieser Form nicht hätte.

Aufgabe Alt: Generator / Callback Empfohlen mit Fibers Vorteil
Async-Aufruf blockierend darstellen then()-Callback-Ketten Fiber::suspend() intern Code liest sich sequenziell, keine Callback-Verschachtelung
Tief verschachtelte Bibliotheksaufrufe yield from in jeder Ebene nur äußerster Aufruf ist Fiber-aware Rest der Aufrufkette bleibt normaler synchroner Code
Fehler aus asynchroner Operation error-first Callback-Parameter Fiber::throw() normale try/catch-Semantik statt Fehlerparameter
Werte zwischen Aufrufer und Coroutine tauschen Iterator-Protokoll mit send() suspend()/resume() bidirektional einfacher, direkter Zwei-Wege-Kanal
Funktion async machen gesamte Aufrufkette "eingefärbt" nur der Einstiegspunkt ändert sich kein "function coloring" mehr
Tausende gleichzeitige Tasks unbegrenzt Fibers ohne Pooling Fiber-Pool mit fester Obergrenze planbarer, begrenzter Speicherverbrauch

Weil der Stapel eines Fibers bei der Erzeugung fest zugeteilt wird, kann tief rekursiver Code innerhalb eines Fibers diesen Stapel vollständig ausschöpfen und mit einem fatalen Fehler abbrechen, ohne dass PHP dies wie bei normalem Speicherhunger elegant abfängt. Stark rekursive Algorithmen sollten in Fiber-lastigen Codepfaden daher iterativ umgeschrieben oder außerhalb des Fibers ausgeführt werden. In Systemen mit hohem Durchsatz empfiehlt sich zusätzlich, Fibers in einem Pool wiederzuverwenden, statt für jede einzelne Anfrage einen neuen zu erzeugen und sofort wieder zu verwerfen.

9. Fibers in der Praxis

Fibers setzen mindestens PHP 8.1 voraus und sind bewusst als niedrig liegende Primitive konzipiert. In den meisten Projekten ist der direkte Umgang mit new Fiber(), start() und resume() gar nicht nötig, weil Bibliotheken wie amphp/amp oder revolt/event-loop diese Mechanik bereits kapseln. Direkter Einsatz lohnt sich vor allem dann, wenn ein Team selbst ein Framework, eine Event-Loop oder einen Coroutine-Scheduler baut, nicht als alltägliches Werkzeug für Anwendungscode.

Wer dennoch direkt mit Fibers arbeitet, sollte niemals einen Fiber unbeendet in der Warteschlange verwaisen lassen und rund um suspend() konsequent try/finally einsetzen, damit Ressourcen wie Locks oder offene Verbindungen auch dann freigegeben werden, wenn ein Fiber vorzeitig abgebrochen wird. Tests für Fiber-basierten Code brauchen einen deterministischen Scheduler, weil ein nicht deterministisches Interleaving in Unit-Tests zu Flakiness führt, die schwer zu reproduzieren ist.

Ein oft übersehener Punkt: Da alle Fibers eines Requests im selben PHP-Prozess laufen, teilen sie sich globalen und statischen Zustand vollständig. Auch ohne echte Parallelität kann ein statischer Zwischenspeicher, der über einen Suspend-Punkt hinweg von mehreren Fibers gelesen und geschrieben wird, zu subtilen Fehlern führen, wenn die Reihenfolge der Fortsetzung nicht exakt der Annahme im Code entspricht.

10. Zusammenfassung

Fibers geben PHP seit Version 8.1 ein echtes Primitive für kooperatives Multitasking: eine Ausführungseinheit mit eigenem Aufrufstapel, die an einem selbst gewählten Punkt pausiert und später mit vollem Kontext fortgesetzt wird. Über start(), resume() und suspend() entsteht ein bidirektionaler Wertekanal zwischen Aufrufer und Fiber, über throw() lassen sich Fehler gezielt an der Suspend-Stelle injizieren. Im Unterschied zu Generatoren funktioniert suspend() aus beliebiger Tiefe der Aufrufkette, was das klassische Problem des "function coloring" auflöst.

Der Preis dieser Flexibilität ist ein eigener, real allozierter Stapel pro Fiber, mit entsprechenden Kosten bei Speicher und dem Risiko eines Stack-Overflows bei stark rekursivem Code. In der Praxis werden Fibers selten direkt in Anwendungscode verwendet, sondern über Bibliotheken wie amphp/amp oder revolt/event-loop konsumiert, die Fibers als Fundament für blockierend aussehenden, aber nicht blockierenden Code nutzen.

Fibers in PHP: Das Wichtigste auf einen Blick

Kernmechanik

start(), resume() und suspend() bilden einen bidirektionalen Kanal zwischen Fiber und Aufrufer, seit PHP 8.1 verfügbar.

Fehlerbehandlung

throw() injiziert eine Exception direkt am Suspend-Punkt des Fibers, normale try/catch-Semantik gilt weiterhin.

Fibers vs. Generatoren

suspend() funktioniert aus jeder Tiefe der Aufrufkette, kein function coloring wie bei yield from mehr nötig.

Kosten & Grenzen

Jeder Fiber besitzt einen eigenen Stapel mit realem Speicherverbrauch, Pooling und iterative Algorithmen sind bei hoher Fiber-Zahl ratsam.

11. FAQ: Fibers in PHP

1Was ist ein Fiber in PHP?
Eine seit PHP 8.1 verfügbare unterbrechbare Ausführungseinheit mit eigenem Aufrufstapel, pausiert über suspend() und fortgesetzt über resume().
2Ab welcher PHP-Version gibt es Fibers?
Seit PHP 8.1. In PHP 8.4 ist die Fiber-Klasse unverändert vorhanden und Basis moderner Coroutine-Bibliotheken.
3Sind Fibers echte Threads?
Nein, ein einziger Prozess und Thread. Kooperatives, nicht präemptives Multitasking, keine parallele CPU-Ausführung.
4Wie tausche ich Werte mit einem Fiber aus?
suspend($v) liefert $v an start()/resume() zurück, resume($v) liefert $v an den wartenden suspend()-Aufruf. Bidirektional.
5Was macht Fiber::throw()?
Wirft eine Exception genau am Suspend-Punkt im Fiber, abfangbar mit try/catch rund um suspend().
6Was unterscheidet Fibers von Generatoren?
suspend() geht aus jeder Tiefe der Aufrufkette, kein yield from auf jeder Ebene nötig, da ein eigener Stapel existiert.
7Wie baue ich einen Scheduler für Fibers?
Warteschlange mit allen nicht terminierten Fibers, reihum starten oder fortsetzen, bis isTerminated() true meldet.
8Wofür eignen sich Fibers in der Praxis?
Blockierend wirkendes, aber nicht blockierendes I/O, Message-Queue-Konsumenten und als Fundament für Coroutine-Bibliotheken.
9Wie teuer ist ein Fiber im Speicher?
Jeder Fiber bekommt einen eigenen allozierten Stapel, real messbarer Speicherverbrauch pro gleichzeitig offenem Fiber.
10Muss ich Fibers direkt benutzen?
Meist nicht, Bibliotheken wie amphp/amp kapseln das bereits. Direkter Einsatz vor allem für Framework- und Bibliotheksautoren.

Mironsoft

PHP-Entwicklung, Performance-Tuning und moderne Backend-Architektur

Nebenläufigen PHP-Code sauber und wartbar bauen?

Wir entwickeln Fiber-basierte Bibliotheken, Event-Loops und Coroutine-Schichten für PHP-Anwendungen, mit klarer Fehlerbehandlung, kontrolliertem Speicherverbrauch und Tests für deterministisches Verhalten.

Architektur-Review

Analyse bestehender Async-Muster und Bewertung, ob Fibers den Code vereinfachen

Scheduler-Entwicklung

Kooperative Scheduler und Event-Loops passgenau für euren Anwendungsfall

Migration

Generator-basierten Legacy-Code schrittweise auf Fibers umstellen