Asynchroniczne promisy
OxPHP uruchamia domknięcia PHP w wątkach w tle, na dedykowanej puli oddzielonej od puli workerów HTTP. Długotrwałe zadania trafiają właśnie tam, zamiast blokować obsługę żądań.
Jak to działa
- Zlecenie — wywołaj
oxphp_async()z domknięciem i opcjonalnymi argumentami. OxPHP serializuje zmienneusedomknięcia oraz argumenty, przekazuje je do puli asynchronicznej i natychmiast zwraca ID promise'a - Wykonanie — dedykowany wątek workera asynchronicznego deserializuje dane, uruchamia domknięcie i serializuje wynik
- Oczekiwanie — wywołaj
oxphp_async_await()z ID promise'a. W trybie worker z Fiberami bieżący Fiber zostaje zawieszony, a inne żądania są nadal obsługiwane na tym samym wątku. W trybie tradycyjnym wątek workera blokuje się, dopóki wynik nie będzie gotowy - Sprzątanie — wszystkie promisy, na które jawnie nie oczekiwano, są automatycznie anulowane i sprzątane na końcu żądania
Konfiguracja
| Zmienna | Domyślnie | Opis |
|---|---|---|
ASYNC_WORKERS |
0 (disabled) |
Liczba dedykowanych wątków workerów asynchronicznych. Ustaw 0, aby całkowicie wyłączyć pulę asynchroniczną |
ASYNC_QUEUE_CAPACITY |
0 (auto) |
Maksymalna liczba oczekujących zadań asynchronicznych. Przy 0 przyjmuje domyślnie ASYNC_WORKERS × 64 |
ASYNC_MAX_FIBERS |
256 |
Limit współbieżnych Fiberów zadań asynchronicznych na jednego workera. Globalny dla procesu limit zadań w toku (w kolejce + uruchomionych) wynosi ASYNC_MAX_FIBERS × ASYNC_WORKERS; zlecenie ponad ten limit jest natychmiast (bez blokowania) odrzucane z OxPHP\Async\AsyncException, dzięki czemu kompozycja typu fan-out nie może zakleszczyć się w oczekiwaniu na pojemność, którą sama zajmuje |
Pula asynchroniczna jest domyślnie wyłączona (ASYNC_WORKERS=0). Przy wyłączonej puli wszystkie cztery funkcje asynchroniczne istnieją, ale wywołane rzucają OxPHP\Async\AsyncException. Ustaw ASYNC_WORKERS na wartość większą niż 0, aby włączyć wykonywanie w tle.
Zlecanie zadań
Przekaż domknięcie i opcjonalne argumenty do oxphp_async(). Funkcja natychmiast zwraca ID promise'a (liczbę całkowitą):
<?php
$promise = oxphp_async(function (string $url) {
return file_get_contents($url);
}, 'https://api.example.com/data');
// The closure is running in the background.
// Do other work here...
$result = oxphp_async_await($promise);
echo $result;Przekazywanie danych do domknięć
Do przekazywania danych używaj zmiennych use lub argumentów funkcji. Obsługiwane są wyłącznie typy skalarne i tablice:
<?php
$apiKey = 'sk-abc123';
$ids = [1, 2, 3];
$promise = oxphp_async(function () use ($apiKey, $ids) {
// $apiKey and $ids are available here
return count($ids);
});Oczekiwanie na wyniki
Pojedynczy promise
<?php
$result = oxphp_async_await($promise); // Wait indefinitely
$result = oxphp_async_await($promise, 5.0); // Wait up to 5 secondsLimit czasu 0.0 (wartość domyślna) oznacza oczekiwanie w nieskończoność. Po przekroczeniu limitu czasu rzucany jest OxPHP\Async\TimeoutException.
Wszystkie promisy
oxphp_async_await_all() czeka na każdy promise i zwraca tablicę asocjacyjną kluczowaną po ID promise'a:
<?php
$p1 = oxphp_async(fn() => file_get_contents('https://api.example.com/users'));
$p2 = oxphp_async(fn() => file_get_contents('https://api.example.com/orders'));
$results = oxphp_async_await_all([$p1, $p2], 10.0);
$users = $results[$p1];
$orders = $results[$p2];oxphp_async_await_all() oczekuje na promisy sekwencyjnie, w kolejności ich występowania w tablicy. Wszystkie domknięcia działają współbieżnie na puli asynchronicznej, ale wątek wywołujący zbiera wyniki pojedynczo.
Pierwszy rozstrzygnięty promise (wyścig)
oxphp_async_await_race() zwraca sterowanie, gdy tylko jeden z promise'ów zostanie rozstrzygnięty — niezależnie od tego, czy został spełniony, czy odrzucony:
<?php
$p1 = oxphp_async(fn() => fetch_from_primary_db());
$p2 = oxphp_async(fn() => fetch_from_replica_db());
$winner = oxphp_async_await_race([$p1, $p2], 5.0);
// $winner = ['id' => int, 'value' => mixed]
echo "Promise {$winner['id']} won: {$winner['value']}";Promisy, które nie wygrały, można nadal oczekiwać pojedynczo po tym, jak oxphp_async_await_race() zwróci sterowanie. Jeśli zwycięski promise został odrzucony, rzucany jest OxPHP\Async\AsyncException — przegrani wciąż pozostają dostępni do oczekiwania. Jest to odpowiednik JavaScriptowego Promise.race.
Pierwszy spełniony promise
oxphp_async_await_any() zwraca sterowanie, gdy tylko jeden z promise'ów zostanie SPEŁNIONY. Odrzucenia są gromadzone i stają się obserwowalne dopiero wtedy, gdy każdy promise zostanie odrzucony. Jest to odpowiednik JavaScriptowego Promise.any — przydatny w scenariuszach awaryjnych / redundancji („dowolne lustro, które odpowie").
<?php
$mirror_a = oxphp_async(fn() => fetch('https://mirror-a.example.com/data'));
$mirror_b = oxphp_async(fn() => fetch('https://mirror-b.example.com/data'));
$mirror_c = oxphp_async(fn() => fetch('https://mirror-c.example.com/data'));
try {
$winner = oxphp_async_await_any([$mirror_a, $mirror_b, $mirror_c], 5.0);
// ['id' => one of the input ids, 'value' => its result]
} catch (\OxPHP\Async\AggregateAsyncException $e) {
foreach ($e->getErrors() as $i => $err) {
// $err keyed by input position 0..N-1
}
foreach ($e->getErrorMap() as $promise_id => $err) {
// $err keyed by promise id
}
} catch (\OxPHP\Async\TimeoutException $e) {
foreach ($e->getPartialErrors() as $promise_id => $err) {
// promises that already rejected before the deadline
}
$cancelled = $e->getCancelledPromiseIds();
// Promise ids that had not settled at the deadline. The cancel flag
// is set on each AND their receivers were dropped — passing any of
// these ids to oxphp_async_await*() afterwards throws "unknown or
// already-awaited promise id". Treat the list as an audit trail, not
// a resumable queue.
}Promisy, które w momencie zwycięstwa wciąż oczekiwały na rozstrzygnięcie, można nadal oczekiwać pojedynczo. Promisy, które zostały odrzucone przed wyłonieniem zwycięzcy, już nie — ich wyniki zostały skonsumowane, gdy zgromadzono je jako kandydackie błędy.
Typy wyjątków
| Klasa | Rzucany przez | Uwagi |
|---|---|---|
OxPHP\Async\AsyncException |
oxphp_async_await(), oxphp_async_await_all(), oxphp_async_await_race() |
Pojedynczy błąd z komunikatem i opcjonalnymi szczegółami oryginalnego wyjątku. |
OxPHP\Async\TimeoutException |
Wszystkie cztery funkcje await-* po przekroczeniu terminu | Rozszerza AsyncException. Dla przekroczeń limitu czasu w oxphp_async_await_any() wypełniane są getPartialErrors() i getCancelledPromiseIds(); w pozostałych miejscach wywołania obie zwracają []. |
OxPHP\Async\AggregateAsyncException |
oxphp_async_await_any(), gdy każdy promise zostanie odrzucony |
Rozszerza AsyncException. Udostępnia getErrors() (pozycyjne, kluczowane 0..N-1), getErrorMap() (kluczowane po ID), getPromiseIds(). |
Obsługa błędów
Wyjątki rzucone wewnątrz asynchronicznego domknięcia są przechwytywane i rzucane ponownie w chwili oczekiwania jako OxPHP\Async\AsyncException:
<?php
$promise = oxphp_async(function () {
throw new \RuntimeException('Something failed');
});
try {
$result = oxphp_async_await($promise);
} catch (\OxPHP\Async\AsyncException $e) {
// "Async task failed: [RuntimeException] Something failed"
echo $e->getMessage();
}exit() i die() wewnątrz asynchronicznego domknięcia również są przechwytywane i konwertowane na OxPHP\Async\AsyncException. Worker asynchroniczny przeżywa i kontynuuje przetwarzanie nowych zadań.
Hierarchia wyjątków
\Exception
└── OxPHP\Async\AsyncException # All async errors
├── OxPHP\Async\TimeoutException # Timeout-specific
└── OxPHP\Async\AggregateAsyncException # Multiple failures (await_all / await_any)Integracja z Fiberami
W trybie worker oxphp_async_await() współpracuje z planistą Fiberów w OxPHP. Zamiast blokować wątek workera, bieżący Fiber zostaje zawieszony na czas oczekiwania na wynik. Planista wznawia go, gdy tylko wynik jest gotowy, dzięki czemu inne żądania są nadal obsługiwane na tym samym wątku.
W trybie tradycyjnym (bez pliku worker) oxphp_async_await() blokuje wątek workera synchronicznie. Worker nie może obsługiwać innych żądań, dopóki czeka.
Dla najlepszej wydajności łącz asynchroniczne promisy z trybem worker:
<?php
// worker.php
require __DIR__ . '/../vendor/autoload.php';
oxphp_worker(function () {
// These two API calls run concurrently on the async pool
// while the fiber suspends — the worker thread is free for other requests
$p1 = oxphp_async(fn() => file_get_contents('https://api.example.com/users'));
$p2 = oxphp_async(fn() => file_get_contents('https://api.example.com/orders'));
$results = oxphp_async_await_all([$p1, $p2]);
echo json_encode($results);
});Kompozycja (zagnieżdżone async)
Zadanie asynchroniczne może samo wywołać oxphp_async() i oczekiwać na wynik. Ponieważ każde zadanie działa wewnątrz Fibera planisty, oczekiwanie na zagnieżdżony promise zawiesza Fiber tego zadania i zwalnia jego workera do uruchomienia zagnieżdżonego zadania — dzięki temu zadanie może rozgałęzić się na potomków bez zajmowania workera na czas oczekiwania.
<?php
$p = oxphp_async(function (): int {
// Dispatched and awaited from inside an async task
$inner = oxphp_async(fn () => 21);
return oxphp_async_await($inner) * 2;
});
$result = oxphp_async_await($p); // 42oxphp_async_await_all(), oxphp_async_await_race() oraz oxphp_async_await_any() również mogą być wywoływane z wnętrza Fibera zadania. Liczba współbieżnych Fiberów zadań (w kolejce + uruchomionych) jest ograniczona przez ASYNC_MAX_FIBERS × ASYNC_WORKERS; zlecenie, które przekroczyłoby ten limit, jest natychmiast odrzucane z OxPHP\Async\AsyncException, zamiast blokować, dzięki czemu fan-out nie może zakleszczyć się w oczekiwaniu na pojemność, którą sam zajmuje.
Ograniczenia
Asynchroniczne domknięcia działają na oddzielnych wątkach. Nakłada to ograniczenia na to, jakie dane mogą przekroczyć granicę wątku:
| Dozwolone | Niedozwolone |
|---|---|
null, bool, int, float, string |
Zwykłe obiekty (dowolna klasa nieimplementująca OxPHP\Shared\Shareable) |
| Tablice typów skalarnych | Zasoby (uchwyty plików, połączenia do baz danych, strumienie) |
| Zagnieżdżone tablice skalarne | Domknięcia, których use przechwytuje obiekty niebędące Shareable |
Instancje Shared\* (Counter, Map, Channel, Atomic, Flag, Mutex, Once, Pool, Registry) oraz inne klasy implementujące OxPHP\Shared\Shareable |
Dodatkowe ograniczenia:
- Tylko funkcje użytkownika — domknięcie musi być zdefiniowane przez użytkownika, a nie być opakowaniem wokół funkcji wbudowanej
- Narzut serializacji — argumenty i wartości zwracane są serializowane przez granicę wątku. Duże tablice lub łańcuchy znaków zwiększają opóźnienie
- Brak stanu współdzielonego dla zwykłych wartości PHP — każdy worker asynchroniczny ma własne środowisko PHP. Zwykłe zmienne, tablice i instancje klas są kopiowane (lub odrzucane) przy przekraczaniu granicy. Aby przekazywać referencje widoczne dla obu wątków, używaj prymitywów stanu współdzielonego (
Shared\Counter,Shared\Map,Shared\Channel, …)
Przykład dla Dockera
services:
app:
image: ghcr.io/oxphp/oxphp:0.10.0
ports:
- "80:80"
environment:
- DOCUMENT_ROOT=/var/www/html/public
- WORKER_MODE_ENABLED=true
- ENTRY_FILE=worker.php
- ASYNC_WORKERS=4
- ASYNC_QUEUE_CAPACITY=256Rozwiązywanie problemów
"Async pool is disabled. Set ASYNC_WORKERS > 0 to enable."
Pula asynchroniczna nie jest skonfigurowana. Gdy ASYNC_WORKERS=0 (wartość domyślna), funkcje asynchroniczne są zarejestrowane, ale przy każdym wywołaniu rzucają OxPHP\Async\AsyncException.
Rozwiązanie: Ustaw ASYNC_WORKERS na wartość dodatnią:
ASYNC_WORKERS=4"Failed to dispatch async task (pool full)"
Pula asynchroniczna działa, ale wszystkie miejsca w kolejce są zajęte lub osiągnięto globalny dla procesu limit zadań w toku (ASYNC_MAX_FIBERS × ASYNC_WORKERS, obejmujący zadania w kolejce + uruchomione). Oba przypadki przy zleceniu rzucają OxPHP\Async\AsyncException i inkrementują oxphp_async_tasks_rejected_total.
Sprawdzenie: Zweryfikuj, czy pula przyjmuje zadania:
curl -s http://localhost:9090/config | jq '.async_workers'Rozwiązanie: Zwiększ ASYNC_WORKERS lub ASYNC_QUEUE_CAPACITY.
"Cannot pass object values in use-vars to async closure"
Obiektów nie można serializować przez granice wątków.
Rozwiązanie: Wyodrębnij potrzebne dane skalarne przed zleceniem:
<?php
// Wrong: passing an object
$promise = oxphp_async(function () use ($user) { ... });
// Correct: passing scalar data extracted from the object
$userId = $user->getId();
$userName = $user->getName();
$promise = oxphp_async(function () use ($userId, $userName) { ... });Oczekiwanie zawiesza się w trybie tradycyjnym
W trybie tradycyjnym oxphp_async_await() blokuje wątek workera. Jeśli wszystkie workery PHP są zablokowane w oczekiwaniu na wyniki asynchroniczne, serwer przestaje przetwarzać żądania.
Rozwiązanie: Włącz tryb worker (WORKER_MODE_ENABLED=true), aby oxphp_async_await() zawieszał Fiber zamiast blokować wątek.
Limity czasu anulują porzucone zadanie
OxPHP\Async\TimeoutException jest rzucany po stronie oczekiwania w chwili przekroczenia terminu. Zadanie w tle nie jest już pozostawiane działające bez nadzoru: zadanie zaparkowane w oxphp_sleep() lub zawieszone w oczekiwaniu na potomny promise zostaje wznowione i rozwija stos (uruchamiają się jego bloki finally), a zadanie obciążające CPU, które nigdy nie oddaje sterowania, jest przerywane na granicy opkodu — dlatego anulowanie działa na zasadzie „best-effort", z krótkim ograniczeniem opóźnienia, a nie natychmiastowo. To samo anulowanie dotyczy promise'ów porzucanych przez oxphp_async_await_all() oraz przegranych z oxphp_async_await_race() / oxphp_async_await_any(). Zadanie, którego mimo to nie da się przerwać na czas, jest osierocone — anulowane, ale dosprzątane na końcu żądania, co może wydłużyć RSHUTDOWN nawet o kilka sekund; obserwuj oxphp_async_tasks_stranded_total.
Dobre praktyki
- Zawsze ustawiaj limity czasu dla wywołań
oxphp_async_await()na produkcji, aby zapobiec nieskończonemu oczekiwaniu - Używaj trybu worker, aby uzyskać nieblokujące oczekiwanie oparte na Fiberach zamiast blokować wątek workera
- Utrzymuj domknięcia małe — zlecaj skupione jednostki pracy, a nie całe obsługi żądań
- Wyodrębniaj skalary przed zleceniem — wyciągaj identyfikatory, łańcuchy znaków i wartości konfiguracyjne z obiektów, zanim przekażesz je do domknięcia
- Monitoruj pulę asynchroniczną — sprawdzaj
oxphp_async_tasks_rejected_totalw metrykach Prometheusa. Jeśli liczba odrzuceń rośnie, zwiększASYNC_WORKERSlubASYNC_QUEUE_CAPACITY
Zobacz też
- Tryb worker — trwałe procesy PHP ze współbieżnością opartą na Fiberach
- Funkcje PHP — dokumentacja
oxphp_async(),oxphp_async_await()i powiązanych funkcji - Metryki — metryki Prometheusa puli asynchronicznej
- Dokumentacja konfiguracji —
ASYNC_WORKERSiASYNC_QUEUE_CAPACITY