Fibers und Amp kombiniert
Das Async/Await Pattern erlaubt es, asynchronen PHP Code so zu schreiben, dass er wie gewöhnlicher, sequenzieller Code aussieht, obwohl er unter der Haube nicht blockiert. Amp baut dieses Pattern auf PHP Fibers auf und ersetzt verschachtelte Callback Ketten durch einfache async und await Aufrufe, ohne die Vorteile echter Nebenläufigkeit zu opfern.
Inhaltsverzeichnis
- 1. Warum Async/Await in PHP überhaupt gebraucht wird
- 2. Fibers als Fundament: suspend und resume unter der Haube
- 3. async() und Future: eine Operation im Hintergrund starten
- 4. await() und der Scheduler: wann tatsächlich gewartet wird
- 5. Mehrere asynchrone Operationen komponieren
- 6. Fehlerbehandlung: Exceptions über Fiber-Grenzen hinweg
- 7. Cancellation und Timeouts sauber umsetzen
- 8. Von verschachtelten Callbacks zu Async/Await migrieren
- 9. Async/Await im Vergleich zu rohen Fibers und Promises
- 10. Zusammenfassung
- 11. FAQ
1. Warum Async/Await in PHP überhaupt gebraucht wird
Das Async/Await Pattern löst ein Lesbarkeitsproblem, das reine Callback oder Promise basierte asynchrone Programmierung fast zwangsläufig mit sich bringt: Sobald mehrere asynchrone Schritte nacheinander ausgeführt werden müssen, verschachteln sich Callbacks tief ineinander oder Promise Ketten werden mit vielen then Aufrufen unübersichtlich. Das Async/Await Pattern löst dieses Problem, indem es asynchronen Code so aussehen lässt wie gewöhnlichen, sequenziellen Code, während die zugrunde liegende Ausführung weiterhin nicht blockierend bleibt.
In PHP wird dieses Async/Await Pattern vor allem durch die Bibliothek Amp umgesetzt, aufbauend auf den seit PHP 8.1 verfügbaren Fibers. Fibers selbst bieten nur das rohe Werkzeug zum Anhalten und Fortsetzen von Ausführungskontexten, definieren aber keine High Level Syntax für asynchrone Programmierung. Amp füllt diese Lücke mit den Funktionen async() und await(), die zusammen ein Async/Await Pattern ergeben, das sich stark an vergleichbare Konzepte in JavaScript oder C# anlehnt, aber vollständig auf PHP Bordmitteln aufbaut.
Der entscheidende Vorteil des Async/Await Pattern gegenüber rohen Fibers oder reinen Promises: Der Code liest sich linear von oben nach unten, Fehlerbehandlung funktioniert mit normalem try catch statt mit separaten Error Callbacks, und die kognitive Last für Entwickler sinkt erheblich. Wo eine Promise Kette fünf verschachtelte then Aufrufe benötigt, genügen beim Async/Await Pattern fünf aufeinanderfolgende await() Aufrufe in einer einzigen linearen Funktion.
2. Fibers als Fundament: suspend und resume unter der Haube
Eine PHP Fiber ist ein unabhängiger Ausführungskontext mit eigenem Stack, der jederzeit über Fiber::suspend() pausiert und später über $fiber->resume() fortgesetzt werden kann. Das Async/Await Pattern, wie Amp es implementiert, nutzt genau diesen Mechanismus: Jeder mit async() gestartete Codeabschnitt läuft in einer eigenen Fiber. Trifft der Code darin auf ein await(), wird die Fiber pausiert, die Kontrolle geht an den Amp Scheduler zurück, und andere Fibers können in der Zwischenzeit weiterlaufen.
Der zentrale Unterschied zwischen rohen Fibers und dem Async/Await Pattern von Amp liegt in der Abstraktion: Wer direkt mit Fibers arbeitet, muss selbst einen Scheduler schreiben, der pausierte Fibers verwaltet und zum richtigen Zeitpunkt fortsetzt. Amp übernimmt diese Scheduler Logik vollständig und bietet stattdessen die einfachen Funktionen async() und await() an, die intern Fiber Suspend und Resume Aufrufe kapseln, ohne dass Anwendungscode jemals direkt mit der Fiber Klasse in Berührung kommt.
Diese Kapselung ist der Grund, warum das Async/Await Pattern in Amp so viel einfacher zu benutzen ist als rohe Fibers. Entwickler schreiben Code, der syntaktisch wie synchrones PHP aussieht, während unter der Haube ein vollständiger kooperativer Scheduler mit Event Loop Integration läuft, der Netzwerk I/O, Timer und Dateizugriffe non blocking abwickelt.
<?php
declare(strict_types=1);
use function Amp\async;
use function Amp\delay;
// async() starts this closure in its own Fiber and returns immediately
$future = async(function (): string {
// delay() suspends the Fiber without blocking the whole process
delay(1.0);
return 'Result after 1 second';
});
echo "This line runs immediately, before the delay finishes\n";
// await() blocks only the current Fiber, not the whole process
$result = $future->await();
echo $result . "\n";
3. async() und Future: eine Operation im Hintergrund starten
Die Funktion async() ist der Einstiegspunkt für das Async/Await Pattern in Amp. Sie nimmt eine Closure entgegen, startet sie sofort in einer neuen Fiber und gibt umgehend ein Future Objekt zurück, ohne auf das Ergebnis zu warten. Das entspricht konzeptionell exakt dem async Schlüsselwort in JavaScript oder C#, nur dass PHP dafür eine Funktion statt einer Sprachsyntax verwendet, weil PHP selbst keine natives Async/Await Schlüsselwort besitzt.
Das Future Objekt, das async() zurückgibt, repräsentiert das noch ausstehende Ergebnis. Es ist konzeptionell mit einem Promise vergleichbar, unterscheidet sich aber in der Art, wie auf das Ergebnis gewartet wird: Statt then() Callbacks zu registrieren, ruft man direkt $future->await() auf, was die aufrufende Fiber pausiert, bis das Ergebnis vorliegt. Dieser direkte Aufruf statt einer Callback Registrierung ist der Kern dessen, was das Async/Await Pattern von reinem Promise basiertem Code unterscheidet.
Ein wichtiges Detail: async() startet die Ausführung sofort, nicht erst beim ersten await(). Das unterscheidet sich von manchen anderen Sprachen, in denen eine async Funktion erst bei Bedarf ausgeführt wird. In Amp läuft der Code innerhalb von async() parallel zum aufrufenden Code weiter, bis er selbst auf ein await() oder delay() trifft, was für das Async/Await Pattern bedeutet, dass mehrere async() Aufrufe direkt nacheinander tatsächlich gleichzeitig zu laufen beginnen.
4. await() und der Scheduler: wann tatsächlich gewartet wird
Die Methode await() auf einem Future Objekt ist die zweite Hälfte des Async/Await Pattern. Sie pausiert die aktuelle Fiber, bis das zugehörige Future entweder erfüllt oder mit einer Exception verworfen wird. Wichtig zu verstehen: await() blockiert nicht den gesamten PHP Prozess, sondern nur die aktuelle Fiber. Der zugrunde liegende Amp Event Loop kann währenddessen andere Fibers weiterlaufen lassen, exakt wie der ReactPHP Event Loop andere Timer und Streams während einer Wartezeit bearbeitet.
Ruft man await() auf ein bereits abgeschlossenes Future auf, kehrt die Funktion sofort zurück, ohne die Fiber tatsächlich zu pausieren. Das Async/Await Pattern in Amp optimiert also automatisch den Fall, in dem das Ergebnis längst vorliegt, und vermeidet unnötige Scheduler Durchläufe. Diese Optimierung ist unsichtbar für den Entwickler, wirkt sich aber messbar auf die Performance bei sehr vielen kurzen asynchronen Operationen aus.
Ein häufiges Missverständnis beim Async/Await Pattern: await() in einem Kontext aufzurufen, der selbst nicht in einer Fiber läuft, etwa direkt im globalen Skript Scope außerhalb von async(), führt zu einem Fehler oder blockiert tatsächlich den gesamten Prozess, je nach Amp Version und Konfiguration. Die sichere Praxis: await() nur innerhalb von Funktionen aufrufen, die selbst über async() gestartet wurden, oder auf der obersten Ebene, wo Amp den Haupt Fiber Kontext bereitstellt.
<?php
declare(strict_types=1);
use function Amp\async;
use Amp\Http\Client\HttpClientBuilder;
use Amp\Http\Client\Request;
function fetchUrl(string $url): string
{
$client = HttpClientBuilder::buildDefault();
$response = $client->request(new Request($url));
// await() suspends only this Fiber while the HTTP response streams in
return $response->getBody()->buffer();
}
// Both requests start concurrently because async() runs them in separate Fibers
$future1 = async(fn (): string => fetchUrl('https://example.com/api/users'));
$future2 = async(fn (): string => fetchUrl('https://example.com/api/orders'));
// await() here blocks only until each specific Future resolves
$users = $future1->await();
$orders = $future2->await();
echo "Users response length: " . strlen($users) . "\n";
echo "Orders response length: " . strlen($orders) . "\n";
5. Mehrere asynchrone Operationen komponieren
Sobald mehrere Future Objekte gleichzeitig verwaltet werden müssen, bietet Amp Hilfsfunktionen, die das Async/Await Pattern um Komposition erweitern. Amp\Future\await(), nicht zu verwechseln mit der Instanzmethode, nimmt ein Array von Futures entgegen und wartet, bis alle erfüllt sind, ähnlich wie Promise.all in JavaScript. Schlägt eines der Futures fehl, wirft die Funktion die erste aufgetretene Exception, während die übrigen Futures im Hintergrund weiterlaufen.
Amp\Future\awaitFirst() löst sich hingegen auf, sobald das erste Future in der übergebenen Liste fertig ist, unabhängig davon, ob es erfolgreich war oder fehlgeschlagen ist, was sich für Race Muster eignet: die schnellste von mehreren redundanten Anfragen gewinnt. Amp\Future\awaitAny() ähnelt dem, ignoriert aber fehlgeschlagene Futures und liefert das erste erfolgreiche Ergebnis, sofern mindestens eines erfolgreich abschließt.
Diese Kompositionsfunktionen sind der Grund, warum das Async/Await Pattern in Amp auch für komplexe, parallele Workflows geeignet bleibt und nicht nur für einfache, sequenzielle Ketten. Ein typisches Beispiel: mehrere unabhängige Datenbank oder API Abfragen parallel starten, alle Ergebnisse mit await() auf dem kombinierten Future Array einsammeln, und erst dann mit der Verarbeitung fortfahren, ohne die Wartezeiten der einzelnen Abfragen aufzusummieren.
6. Fehlerbehandlung: Exceptions über Fiber-Grenzen hinweg
Einer der größten praktischen Vorteile des Async/Await Pattern gegenüber reinen Callback Ketten liegt in der Fehlerbehandlung. Wirft eine Closure innerhalb von async() eine Exception, wird diese nicht sofort geworfen, sondern im Future Objekt gespeichert und erst beim Aufruf von await() im aufrufenden Kontext erneut geworfen. Das erlaubt es, ganz normale try catch Blöcke um await() Aufrufe zu legen, exakt wie bei synchronem Code.
Diese Eigenschaft des Async/Await Pattern vereinfacht Fehlerbehandlung erheblich gegenüber Promise basiertem Code, wo Fehler über separate catch Callbacks oder den zweiten Parameter von then() behandelt werden müssen, oft an einer anderen Stelle im Code als der eigentliche fehlerhafte Aufruf. Mit await() steht die Fehlerbehandlung direkt neben dem Code, der den Fehler verursachen könnte, was Debugging und Code Review deutlich erleichtert.
Wichtig zu beachten: Wird das Future einer fehlgeschlagenen async() Operation niemals mit await() abgerufen, geht die Exception verloren, ohne dass sie irgendwo protokolliert wird, ein sogenannter unhandled rejection ähnlich wie bei JavaScript Promises. Beim Async/Await Pattern in Amp ist es deshalb wichtig, jedes gestartete Future irgendwann tatsächlich mit await() abzuholen, auch wenn dessen Ergebnis nicht direkt benötigt wird, allein um Fehler nicht stillschweigend zu verlieren.
7. Cancellation und Timeouts sauber umsetzen
Lang laufende asynchrone Operationen brauchen einen Weg, vorzeitig abgebrochen zu werden, etwa wenn ein Nutzer eine Anfrage abbricht oder ein Timeout erreicht wird. Amp implementiert das im Async/Await Pattern über Cancellation Objekte, die an Funktionen übergeben werden, welche selbst auf Abbruch reagieren können. Eine TimeoutCancellation bricht eine Operation automatisch nach einer festgelegten Zeitspanne ab, eine DeferredCancellation erlaubt manuellen Abbruch durch Anwendungscode, etwa nach einem Nutzer Klick.
Der entscheidende Unterschied zu einfachem Timeout Handling mit Timern: Ein Cancellation Token im Async/Await Pattern wird durch die gesamte Aufrufkette weitergereicht, sodass verschachtelte asynchrone Operationen alle gemeinsam abgebrochen werden können, nicht nur die äußerste. Eine HTTP Anfrage, die intern mehrere Unterschritte ausführt, kann so bei Zeitüberschreitung sauber und vollständig gestoppt werden, statt nur den äußeren Aufruf zu beenden und innere Operationen im Hintergrund weiterlaufen zu lassen.
Ohne konsequente Cancellation Unterstützung würde jede Anwendung mit dem Async/Await Pattern Gefahr laufen, Ressourcen für längst nicht mehr benötigte Operationen zu verschwenden, etwa wenn ein Client die Verbindung trennt, aber die Serverseitig gestarteten Futures unbeirrt weiterlaufen und Datenbankverbindungen oder externe API Kontingente belegen, obwohl niemand mehr auf das Ergebnis wartet.
<?php
declare(strict_types=1);
use function Amp\async;
use function Amp\delay;
use Amp\TimeoutCancellation;
use Amp\CancelledException;
function slowOperation(\Amp\Cancellation $cancellation): string
{
// delay() respects cancellation and throws if the timeout fires
delay(5.0, cancellation: $cancellation);
return 'Completed successfully';
}
$cancellation = new TimeoutCancellation(2.0);
try {
$result = async(fn (): string => slowOperation($cancellation))->await();
echo $result . "\n";
} catch (CancelledException) {
echo "Operation timed out after 2 seconds\n";
}
8. Von verschachtelten Callbacks zu Async/Await migrieren
Bestehende Codebasen, die auf ReactPHP Promises oder manuell verschachtelten Callbacks aufbauen, lassen sich schrittweise auf das Async/Await Pattern migrieren, ohne die gesamte Anwendung auf einmal umschreiben zu müssen. Amp bietet Adapterfunktionen, die ein ReactPHP Promise in ein Amp Future umwandeln und umgekehrt, sodass beide Systeme parallel koexistieren können, während einzelne Module nach und nach umgestellt werden.
Der pragmatische Migrationsweg beginnt meist an den Blättern des Aufrufbaums: einzelne, isolierte Funktionen, die aktuell Callbacks nehmen, werden zuerst auf async() und await() umgestellt, während der sie umgebende Code unverändert bleibt und die neue Funktion einfach wie eine synchrone Funktion aufruft. Erst wenn genügend Blattfunktionen migriert sind, lohnt sich der Umbau der darüber liegenden Orchestrierungsschicht auf das vollständige Async/Await Pattern.
Ein Vorteil dieser schrittweisen Migration: Tests für einzelne migrierte Funktionen werden gleichzeitig deutlich einfacher, weil asynchroner Code, der wie synchroner Code aussieht, sich auch fast identisch testen lässt, ohne komplizierte Callback Mocking Konstruktionen. Diese Testbarkeit ist häufig der eigentliche Auslöser dafür, dass Teams das Async/Await Pattern gegenüber reinen Promise Ketten bevorzugen, unabhängig von den reinen Performance Eigenschaften.
9. Async/Await im Vergleich zu rohen Fibers und Promises
Das Async/Await Pattern ist eine von mehreren möglichen Abstraktionsebenen über PHP Fibers. Welche Ebene angemessen ist, hängt vom Kontrollbedarf und der gewünschten Lesbarkeit des Codes ab.
| Ansatz | Lesbarkeit | Fehlerbehandlung | Kontrolle |
|---|---|---|---|
| Rohe Fibers | Niedrig, manuelles suspend/resume | Manuell, kein Standard | Maximal, voller Zugriff |
| Promises (react/promise) | Mittel, then-Ketten | Separate catch-Callbacks | Gut, aber verschachtelt |
| Async/Await (Amp) | Hoch, linearer Code | Normales try/catch | Gut, mit Cancellation |
| Generators mit yield | Mittel, eigene Syntax | Möglich, aber ungewohnt | Gut, aber verbreitet weniger |
Für neue Projekte, die asynchrone I/O Operationen brauchen, ist das Async/Await Pattern mit Amp in den meisten Fällen die richtige Wahl: Es bietet die Lesbarkeit synchronen Codes bei der vollen Leistungsfähigkeit non blockierender Ausführung. Rohe Fibers bleiben relevant für Bibliotheksautoren, die eigene Abstraktionen bauen wollen, während einfache Promise Ketten oft schon für kleinere Projekte mit wenigen asynchronen Schritten ausreichen.
Mironsoft
Asynchrone PHP Architekturen mit Amp und Fibers
Asynchroner Code, der lesbar bleibt wie synchroner Code?
Wir migrieren verschachtelte Callback Strukturen auf das Async/Await Pattern mit Amp, inklusive sauberer Fehlerbehandlung, Cancellation und Timeout Handling für produktionsreife asynchrone Dienste.
Code-Migration
Schrittweiser Umbau von Callbacks und Promises auf async()/await()
Fehler & Cancellation
Try/catch basierte Fehlerbehandlung und robustes Timeout Handling
Architektur-Beratung
Auswahl zwischen Fibers, Amp und ReactPHP je nach Anwendungsfall
10. Zusammenfassung
Das Async/Await Pattern in PHP, umgesetzt durch Amp auf Basis von Fibers, löst das Lesbarkeitsproblem klassischer Callback und Promise Ketten, ohne die Vorteile nicht blockierender Ausführung zu opfern. async() startet Code in einer eigenen Fiber, await() pausiert nur den aufrufenden Kontext statt des gesamten Prozesses, und Fehlerbehandlung funktioniert mit gewöhnlichem try catch statt separaten Error Callbacks.
Kompositionsfunktionen wie await() über Arrays von Futures, awaitFirst() und awaitAny() erweitern das Async/Await Pattern um parallele Workflows, während Cancellation Tokens ein sauberes, kaskadierendes Abbrechen lang laufender Operationen ermöglichen. Für neue asynchrone PHP Projekte ist dieses Pattern heute meist die richtige Wahl, weil es die Verständlichkeit synchronen Codes mit der vollen Leistungsfähigkeit asynchroner I/O verbindet.
Async/Await Pattern mit Fibers und Amp — Das Wichtigste auf einen Blick
Grundprinzip
async() startet eine Fiber sofort, await() pausiert nur den aufrufenden Kontext bis zum Ergebnis.
Fehlerbehandlung
Exceptions werden im Future gespeichert und bei await() erneut geworfen. Normales try/catch möglich.
Komposition
Future\await(), awaitFirst() und awaitAny() koordinieren mehrere parallele Operationen.
Cancellation
TimeoutCancellation und DeferredCancellation reichen Abbruchsignale durch verschachtelte Aufrufe.