TrueAsync in Laravel: Architektur für echte Parallelität ohne Swoole
Traditionelle Lösungen für Asynchronität in PHP, wie Swoole oder RoadRunner, erfordern umfangreiche Code-Änderungen und die Einführung von gefärbten Funktionen. TrueAsync bietet einen grundlegend anderen Ansatz – die Modifikation des Zend-Kerns für transparente Asynchronität. Wir erklären Schritt für Schritt, wie man Laravel an dieses Modell anpasst, Probleme mit zustandsbehafteten Diensten löst und parallele Verarbeitung von I/O-gebundenen Operationen ermöglicht.
Grundlagen von TrueAsync: So funktioniert transparente Asynchronität
TrueAsync ist ein alternativer PHP-Kern, der den Zend Engine, I/O-Bibliotheken und Datenbankoperationen modifiziert. Der entscheidende Vorteil: Das Problem der gefärbten Funktionen entfällt. Im Gegensatz zu Node.js oder Python asyncio geben Standard-PHP-Funktionen (PDO::query(), file_get_contents()) bei I/O-Wartezeiten automatisch die Kontrolle an den Scheduler ab. Coroutines hängen auf C-Funktions-Ebene ein und ermöglichen die Bearbeitung Hunderter Anfragen in einem einzigen Thread.
Wichtig zu verstehen: Das ist keine Multithreading-Lösung. Das Modell basiert auf kooperativem Multitasking – Kontextwechsel beim Warten auf externe Ressourcen. Es eignet sich nicht für CPU-intensive Aufgaben (schwere Berechnungen), ist aber ideal für typische Web-Szenarien mit Datenbank- und API-Aufrufen. Hier ein einfaches Beispiel:
use function Async\spawn;
use function Async\await;
use function Async\delay;
$a = spawn(function() {
delay(100);
return 'result A';
});
$b = spawn(function() {
delay(100);
return 'result B';
});
// Obschee time vypolneniya ~100ms
echo await($a);
echo await($b);
Das Problem der zustandsbehafteten Dienste in Laravel
Laravel wurde ursprünglich für das FPM-Modell entwickelt, bei dem jede Anfrage in einem isolierten Prozess bearbeitet wird. Zustandsbehaftete Dienste (AuthManager, Session, Router) speichern ihren Zustand in Objekteigenschaften, die nach der Anfragezerbearbeitung zerstört werden. Im Async-Modus lebt der Prozess unbefristet weiter, was zu Zustandslecks führt:
// Coroutine A: request from user_1
Auth::loginUsingId(1);
delay(200);
// Coroutine B: request from user_2
Auth::loginUsingId(2);
// Return in korutinu A
echo Auth::id(); // Vozvraschaet 2 vmesto 1
Statische Eigenschaften (Model::$dispatcher, Facade::$resolvedInstance) und Dienste im IoC-Container werden über alle Anfragen hinweg geteilt. Octane umgeht das durch Klonen des Containers, was jedoch Overhead erzeugt und statische Eigenschaften nicht abdeckt.
Kontextuelle Isolation: Die Lösungsarchitektur
TrueAsync stellt current_context() bereit – einen Datenspeicher, der an den Anfragelebenszyklus gebunden ist. Bei Abschluss einer Coroutine wird der Kontext automatisch geleert. Die Architektur teilt Laravel in zwei Zonen auf:
- Statischer Kern: Router, Config, IoC-Container. Einmal geladen, über alle Anfragen geteilt.
- Pro-Anfrage-Dienste: Auth, Session, Cookie. In jedem Coroutine-Kontext erzeugt.
Der zentrale Mechanismus ist die Abfangung der Dienste-Auflösung über AsyncApplication::resolve():
protected function resolve($abstract, ...)
{
if ($this->asyncMode && $this->isScoped($abstract)) {
$ctx = current_context();
if ($instance = $ctx->find($abstract)) {
return $instance;
}
$instance = $this->buildFresh($abstract);
$ctx->set($abstract, $instance);
return $instance;
}
return parent::resolve($abstract, ...);
}
Kontextuelle Schlüssel über Enum und die Facade-Lösung
Um Kollisionen zu vermeiden und sicheren Zugriff zu gewährleisten, kommen Enum-Schlüssel zum Einsatz:
enum ScopedService: string
{
case REQUEST = 'request';
case SESSION = 'session';
case AUTH = 'auth';
case AUTH_DRIVER = 'auth.driver';
case COOKIE = 'cookie';
}
Das bietet:
- Zugriffs-Isolation (nur der Enum-Besitzer kann Daten lesen/schreiben)
- Kollisionsfreie Garantien (auch bei identischen String-Werten)
- Unterstützung für statische Analyse (PHPStan/IDE verfolgt die Nutzung)
Fassade erforderte eine separate Lösung wegen des Cachings von resolvedInstance in einem statischen Array. Hier kommt ScopedServiceProxy ins Spiel:
class ScopedServiceProxy
{
public function __call($method, $args)
{
return ($this->resolver)()->$method(...$args);
}
}
Der Proxy leitet Aufrufe an den Kontext der aktuellen Coroutine weiter, während der Cache auf Proxy-Ebene erhalten bleibt.
Kritische Anpassungen und PDO-Pool
Nach der grundlegenden Isolation wurden Dienste identifiziert, die intern pro-Anfrage-Zustand speichern und spezielle Async-Versionen benötigen:
- AsyncRouter (Isolation der aktuellen Route)
- AsyncDispatcher (ereignisverzögerte Events pro Coroutine)
- AsyncTranslator (Übersetzungs-Locale)
- AsyncViewFactory (View::share())
- AsyncConfig (Laufzeit-Konfigurationsänderungen)
- AsyncDatabaseSessionHandler (atomare Session-Schreibvorgänge)
- Async*Connection (Transaktionszähler)
Besondere Aufmerksamkeit galt der Datenbankbehandlung. Ohne Connection Pool brechen gleichzeitige Coroutine-Abfragen das PostgreSQL-Protokoll. TrueAsync löst das mit einem integrierten PDO-Pool:
$pdo = new PDO($dsn, $user, $password, [
PDO::ATTR_POOL_ENABLED => true,
PDO::ATTR_POOL_MIN => 0,
PDO::ATTR_POOL_MAX => 10,
PDO::ATTR_POOL_HEALTHCHECK_INTERVAL => 30,
]);
Der Pool verteilt Verbindungen dynamisch: Nach einem SELECT-Ergebnis kehrt die Verbindung in den Pool zurück und bedient mehr Coroutines als die maximale Pool-Größe. Bei Transaktionen bleibt die Verbindung bis commit/rollback gesperrt.
Wichtige Erkenntnisse
- TrueAsync eliminiert gefärbte Funktionen durch PHP-Kern-Modifikationen
- Zustandsbehaftete Laravel-Dienste erfordern kontextuelle Isolation über current_context()
- Der PDO-Pool ist essenziell für korrekte Datenbanknutzung in parallelen Umgebungen
- Enum-Schlüssel gewährleisten Sicherheit und Nachverfolgbarkeit kontextueller Daten
- Die Lösung ersetzt kein Multithreading für CPU-intensive Aufgaben
— Editorial Team
Noch keine Kommentare.