26 Fibers

Fibers, eingeführt in PHP 8.1, sind ein leistungsstarkes Sprachmerkmal, das kooperatives Multitasking in PHP ermöglicht. Sie bieten eine flexible Möglichkeit, die Ausführung von Code zu unterbrechen und später an genau dieser Stelle fortzusetzen, ohne komplexe Callback-Strukturen oder separate Prozesse zu benötigen.

26.1 Grundkonzept von Fibers

Eine Fiber stellt einen ausführbaren Codeblock dar, dessen Ausführung an beliebigen Stellen unterbrochen und später fortgesetzt werden kann. Im Gegensatz zu Threads laufen Fibers nicht parallel – die Kontrolle wird explizit durch den Entwickler übergeben.

$fiber = new Fiber(function (): void {
    echo "Fiber gestartet\n";
    
    $value = Fiber::suspend('unterbrochen');
    
    echo "Fiber fortgesetzt mit Wert: $value\n";
});

$result = $fiber->start();
echo "Hauptprogramm erhielt: $result\n";

$fiber->resume('fortsetzen');
echo "Fiber abgeschlossen\n";

Ausgabe:

Fiber gestartet
Hauptprogramm erhielt: unterbrochen
Fiber fortgesetzt mit Wert: fortsetzen
Fiber abgeschlossen

26.2 Kernfunktionalitäten von Fibers

26.2.1 1. Erstellen und Starten einer Fiber

// Eine Fiber erstellen
$fiber = new Fiber(function () {
    echo "In der Fiber\n";
    return "Fiber-Ergebnis";
});

// Die Fiber starten
$result = $fiber->start();
echo "Fiber beendet mit: $result\n";

26.2.2 2. Unterbrechen und Daten zurückgeben

Mit Fiber::suspend() kann die Ausführung unterbrochen und ein Wert an den aufrufenden Code zurückgegeben werden:

$fiber = new Fiber(function () {
    echo "Schritt 1\n";
    Fiber::suspend("Nach Schritt 1");
    
    echo "Schritt 2\n";
    Fiber::suspend("Nach Schritt 2");
    
    echo "Schritt 3\n";
    return "Abgeschlossen";
});

$result = $fiber->start();
echo "Ergebnis 1: $result\n";

$result = $fiber->resume();
echo "Ergebnis 2: $result\n";

$result = $fiber->resume();
echo "Ergebnis 3: $result\n";

26.2.3 3. Daten an eine Fiber übergeben

Bei der Fortsetzung können Daten an die Fiber übergeben werden:

$fiber = new Fiber(function () {
    $received = Fiber::suspend("Daten benötigt");
    echo "Fiber erhielt: $received\n";
    
    return "Fertig";
});

$message = $fiber->start();
echo "Fiber sagt: $message\n";

$result = $fiber->resume("Hier sind die Daten");
echo "Abschlussergebnis: $result\n";

26.2.4 4. Behandlung von Exceptions

Exceptions können sowohl innerhalb als auch außerhalb der Fiber behandelt werden:

$fiber = new Fiber(function () {
    try {
        echo "Vor Exception\n";
        Fiber::suspend("Punkt 1");
        
        throw new Exception("Fehler in Fiber");
    } catch (Exception $e) {
        return "Exception gefangen: " . $e->getMessage();
    }
});

try {
    $value = $fiber->start();
    echo "Fiber unterbrochen mit: $value\n";
    
    $result = $fiber->resume();
    echo "Fiber ergebnis: $result\n";
} catch (Throwable $e) {
    echo "Exception außerhalb gefangen: " . $e->getMessage() . "\n";
}

26.3 Praktische Anwendungsfälle für Fibers

26.3.1 1. Asynchrone I/O-Operationen

Fibers sind ideal für die Implementierung von asynchronen I/O-Operationen, bei denen während des Wartens auf Daten andere Aufgaben erledigt werden können:

function asyncRead(string $filename): Fiber {
    return new Fiber(function () use ($filename) {
        echo "Starte Lesen von $filename...\n";
        
        // Simuliere asynchronen Dateizugriff
        Fiber::suspend("Leseoperation gestartet");
        
        // In einer echten Implementierung würde hier ein Event-Loop die Fiber fortsetzen,
        // wenn die Daten verfügbar sind
        $content = file_get_contents($filename);
        
        return $content;
    });
}

// Mehrere Dateien "gleichzeitig" lesen
$fibers = [];
$fileNames = ['file1.txt', 'file2.txt', 'file3.txt'];

foreach ($fileNames as $file) {
    $fiber = asyncRead($file);
    $status = $fiber->start();
    echo "$file: $status\n";
    $fibers[$file] = $fiber;
}

// In einer echten Implementierung würde hier ein Event-Loop die Fibers fortsetzen
foreach ($fibers as $file => $fiber) {
    $content = $fiber->resume();
    echo "$file enthält: " . substr($content, 0, 50) . "...\n";
}

26.3.2 2. Generatoren mit bidirektionalem Datenaustausch

Fibers ermöglichen eine verbesserte Version von Generatoren, bei denen Daten in beide Richtungen fließen können:

function sequenceGenerator(): Fiber {
    return new Fiber(function () {
        $n = 0;
        while (true) {
            $command = Fiber::suspend($n++);
            
            if ($command === 'reset') {
                $n = 0;
            } elseif ($command === 'stop') {
                break;
            }
        }
        
        return "Generator beendet";
    });
}

$generator = sequenceGenerator();
for ($i = 0; $i < 5; $i++) {
    echo $generator->start() . "\n";  // Für den ersten Wert
    echo $generator->resume('next') . "\n";
}

echo $generator->resume('reset') . "\n";
echo $generator->resume('next') . "\n";
echo $generator->resume('stop') . "\n";

26.3.3 3. Pipeline-Verarbeitung

Fibers können verwendet werden, um Datenverarbeitungspipelines zu erstellen, in denen jede Stufe Daten verarbeitet und an die nächste weitergibt:

function createPipelineStage(Closure $processor, ?Fiber $next = null): Fiber {
    return new Fiber(function () use ($processor, $next) {
        while (true) {
            $data = Fiber::suspend("Bereit");
            
            // Daten verarbeiten
            $result = $processor($data);
            
            // An die nächste Stufe weiterleiten, falls vorhanden
            if ($next !== null) {
                $next->resume($result);
            } else {
                Fiber::suspend($result);  // Letztes Ergebnis zurückgeben
            }
        }
    });
}

// Pipeline erstellen
$stage3 = createPipelineStage(fn($data) => "Ergebnis: $data");
$stage2 = createPipelineStage(fn($data) => strtoupper($data), $stage3);
$stage1 = createPipelineStage(fn($data) => "Verarbeitet: $data", $stage2);

// Pipeline initialisieren
$stage3->start();
$stage2->start();
$status = $stage1->start();
echo $status . "\n";

// Daten durch die Pipeline schicken
$stage1->resume("Testdaten");

// Ergebnis aus der letzten Stufe abholen
$result = $stage3->suspend();
echo $result . "\n";

26.4 Fibers vs. Generatoren

PHP verfügte bereits vor der Einführung von Fibers über Generatoren. Beide haben Ähnlichkeiten, unterscheiden sich jedoch in wichtigen Aspekten:

// Generator-Beispiel
function generatorExample() {
    yield "Erster Wert";
    $received = yield "Zweiter Wert";
    yield "Erhielt: $received";
}

$generator = generatorExample();
echo $generator->current() . "\n";  // "Erster Wert"
$generator->next();
echo $generator->current() . "\n";  // "Zweiter Wert"
$generator->send("Hallo Generator");
echo $generator->current() . "\n";  // "Erhielt: Hallo Generator"

// Äquivalentes Fiber-Beispiel
$fiber = new Fiber(function () {
    Fiber::suspend("Erster Wert");
    $received = Fiber::suspend("Zweiter Wert");
    Fiber::suspend("Erhielt: $received");
});

echo $fiber->start() . "\n";        // "Erster Wert"
echo $fiber->resume() . "\n";       // "Zweiter Wert"
echo $fiber->resume("Hallo Fiber") . "\n";  // "Erhielt: Hallo Fiber"

Die wichtigsten Unterschiede:

  1. Flexibilität: Fibers können an jeder beliebigen Stelle im Code unterbrochen werden, auch innerhalb verschachtelter Funktionsaufrufe. Generatoren können nur durch yield innerhalb der Generator-Funktion unterbrochen werden.

  2. Verschachtelung: Fibers können andere Fibers aufrufen und unterbrechen, was mit Generatoren komplizierter zu implementieren ist.

  3. Kontrolle: Fibers bieten eine feingranularere Kontrolle über den Ausführungsfluss.

  4. Rückgabewerte: Fibers können komplexere Rückgabemuster haben und sind nicht auf die yield-Syntax beschränkt.

26.5 Fibers und asynchrone Frameworks

Fibers sind besonders wertvoll für die Implementierung von asynchronen Frameworks und Bibliotheken. Sie ermöglichen eine einfachere Nutzung von asynchronem Code, der wie synchroner Code aussieht:

// Hypothetisches asynchrones Framework mit Fibers
$app = new AsyncApp();

// Ohne Fibers (mit Callbacks oder Promises)
$app->get('/data', function ($request, $response) use ($db) {
    $db->query('SELECT * FROM users')
        ->then(function ($users) use ($response) {
            return $response->json($users);
        })
        ->catch(function ($error) use ($response) {
            return $response->error($error);
        });
});

// Mit Fibers (sieht wie synchroner Code aus)
$app->get('/data', function ($request, $response) use ($db) {
    try {
        $users = $db->query('SELECT * FROM users');  // Asynchron, aber sieht synchron aus
        return $response->json($users);
    } catch (Exception $e) {
        return $response->error($e);
    }
});

In diesem Beispiel verwendet das hypothetische Framework intern Fibers, um die asynchrone Datenbankabfrage zu ermöglichen, während der Code für den Entwickler wie normaler synchroner Code aussieht.

26.6 Herausforderungen und Einschränkungen von Fibers

26.6.1 1. Debugging-Komplexität

Das Debugging von Fibers kann komplex sein, da der Ausführungsfluss nicht linear ist:

$fiber = new Fiber(function () {
    $a = 1;
    echo "Vor Unterbrechung: a = $a\n";
    
    Fiber::suspend();
    
    $a++;
    echo "Nach Fortsetzung: a = $a\n";
});

$fiber->start();

// Viele Zeilen Code später...
$fiber->resume();

26.6.2 2. Speicherverwaltung

Fibers behalten ihren gesamten Ausführungszustand, was zu erhöhtem Speicherverbrauch führen kann:

function createManyFibers($count) {
    $fibers = [];
    for ($i = 0; $i < $count; $i++) {
        $fibers[] = new Fiber(function () use ($i) {
            // Große Datenstrukturen erstellen
            $data = array_fill(0, 1000, "Daten für Fiber $i");
            
            Fiber::suspend("Fiber $i erstellt");
            
            // Mehr mit den Daten machen
            return "Fiber $i abgeschlossen";
        });
    }
    return $fibers;
}

// Könnte bei vielen Fibers speicherintensiv werden
$fibers = createManyFibers(1000);
foreach ($fibers as $fiber) {
    $fiber->start();
}

26.6.3 3. Kein echtes Multithreading

Fibers bieten kooperatives Multitasking, aber keinen echten Parallelismus:

$fiber1 = new Fiber(function () {
    // Rechenintensiver Task
    for ($i = 0; $i < 1000000; $i++) {
        // Diese Schleife blockiert weiterhin, bis sie abgeschlossen ist
        // Es gibt keine automatische Unterbrechung
    }
    return "Fertig";
});

// Fiber2 wird erst ausgeführt, wenn Fiber1 abgeschlossen oder unterbrochen ist
$fiber2 = new Fiber(function () {
    return "Ich muss warten, bis Fiber1 unterbrochen wird oder fertig ist";
});

26.7 Best Practices für die Verwendung von Fibers

26.7.1 1. Klare Unterbrechungspunkte definieren

Gestalten Sie Ihren Code so, dass die Unterbrechungspunkte klar sind und dokumentieren Sie das erwartete Verhalten:

class AsyncOperation {
    private Fiber $fiber;
    
    public function __construct(callable $operation) {
        $this->fiber = new Fiber(function () use ($operation) {
            $result = null;
            
            try {
                // Dokumentierte Unterbrechungspunkte
                Fiber::suspend(['status' => 'started']);
                
                // Hauptoperation ausführen
                $result = ($operation)();
                
                // Fortschritt melden
                Fiber::suspend(['status' => 'completed', 'result' => $result]);
            } catch (Throwable $e) {
                Fiber::suspend(['status' => 'error', 'error' => $e->getMessage()]);
            }
            
            return $result;
        });
    }
    
    public function start() {
        return $this->fiber->start();
    }
    
    public function resume() {
        if ($this->fiber->isTerminated()) {
            throw new RuntimeException("Operation already completed");
        }
        
        return $this->fiber->resume();
    }
}

26.7.2 2. Status überwachen

Verwenden Sie die Fiber-Statusmethoden, um den aktuellen Zustand einer Fiber zu überprüfen:

function fiberStatus(Fiber $fiber): string {
    if ($fiber->isStarted()) {
        if ($fiber->isTerminated()) {
            return "Terminated";
        } elseif ($fiber->isSuspended()) {
            return "Suspended";
        } else {
            return "Running";
        }
    } else {
        return "Not started";
    }
}

$fiber = new Fiber(function () {
    echo "Status in Fiber vor suspend: " . fiberStatus(Fiber::getCurrent()) . "\n";
    Fiber::suspend();
    echo "Status in Fiber nach resume: " . fiberStatus(Fiber::getCurrent()) . "\n";
});

echo "Status vor Start: " . fiberStatus($fiber) . "\n";
$fiber->start();
echo "Status nach Start: " . fiberStatus($fiber) . "\n";
$fiber->resume();
echo "Status nach Ende: " . fiberStatus($fiber) . "\n";

26.7.3 3. Strukturieren Sie komplexe Fiber-basierte Systeme

Für komplexe Systeme sollten Sie Fibers in höherer Abstraktion kapseln:

class AsyncScheduler {
    private array $readyQueue = [];
    private array $waitingIo = [];
    
    public function schedule(Fiber $fiber): void {
        $this->readyQueue[] = $fiber;
    }
    
    public function waitForIo(string $resource, Fiber $fiber): void {
        $this->waitingIo[$resource] = $fiber;
    }
    
    public function ioCompleted(string $resource, $data): void {
        if (isset($this->waitingIo[$resource])) {
            $fiber = $this->waitingIo[$resource];
            unset($this->waitingIo[$resource]);
            $this->schedule($fiber);
            // In einer echten Implementierung würden wir die Daten speichern
            // und beim Fortsetzen an die Fiber übergeben
        }
    }
    
    public function run(): void {
        while (!empty($this->readyQueue)) {
            $fiber = array_shift($this->readyQueue);
            
            if (!$fiber->isStarted()) {
                $fiber->start();
            } elseif ($fiber->isSuspended()) {
                $fiber->resume();
            }
            
            // In einer realen Implementierung würden wir prüfen,
            // ob die Fiber beendet oder nur unterbrochen wurde
        }
    }
}