非同期 Promise
OxPHP は PHP のクロージャを、HTTP ワーカープールとは別の専用プール上のバックグラウンドスレッドで実行します。時間のかかる処理は、リクエスト処理をブロックする代わりに、そちらで実行されます。
仕組み
- ディスパッチ —
oxphp_async()をクロージャと任意の引数とともに呼び出します。OxPHP はクロージャのuse変数と引数をシリアライズして非同期プールに送信し、即座に Promise ID を返します - 実行 — 専用の非同期ワーカースレッドがデータをデシリアライズし、クロージャを実行して、結果をシリアライズします
- 待機(Await) —
oxphp_async_await()を Promise ID とともに呼び出します。ファイバーを使用するワーカーモードでは、現在のファイバーが中断され、他のリクエストが同じスレッド上で処理を継続します。従来モードでは、結果が準備できるまでワーカースレッドがブロックします - クリーンアップ — 明示的に待機されなかった Promise は、リクエストの終了時に自動的にキャンセルされ、クリーンアップされます
設定
| 変数 | デフォルト | 説明 |
|---|---|---|
ASYNC_WORKERS |
0(無効) |
専用の非同期ワーカースレッドの数。0 に設定すると非同期プールを完全に無効化します |
ASYNC_QUEUE_CAPACITY |
0(自動) |
保留中の非同期タスクの最大数。0 の場合、デフォルトで ASYNC_WORKERS × 64 になります |
ASYNC_MAX_FIBERS |
256 |
ワーカーごとの同時非同期タスクファイバー数の上限。プロセス全体の処理中(in-flight)の上限(キュー内 + 実行中のタスク)は ASYNC_MAX_FIBERS × ASYNC_WORKERS です。これを超えるディスパッチは OxPHP\Async\AsyncException とともに即座に(ノンブロッキングで)拒否されるため、ファンアウトのコンポジションが自身の保持する容量を待機してデッドロックに陥ることはありません |
非同期プールはデフォルトで無効です(ASYNC_WORKERS=0)。プールが無効の場合、4 つの非同期関数はすべて存在しますが、呼び出されると OxPHP\Async\AsyncException をスローします。バックグラウンド実行を有効にするには、ASYNC_WORKERS を 0 より大きい値に設定してください。
タスクのディスパッチ
クロージャと任意の引数を oxphp_async() に渡します。即座に Promise ID(整数)が返されます。
<?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;クロージャへのデータの受け渡し
データを渡すには use 変数または関数の引数を使用します。サポートされるのはスカラー型と配列のみです。
<?php
$apiKey = 'sk-abc123';
$ids = [1, 2, 3];
$promise = oxphp_async(function () use ($apiKey, $ids) {
// $apiKey and $ids are available here
return count($ids);
});結果の待機
単一の Promise
<?php
$result = oxphp_async_await($promise); // Wait indefinitely
$result = oxphp_async_await($promise, 5.0); // Wait up to 5 secondsタイムアウト 0.0(デフォルト)は無期限に待機します。タイムアウト時には OxPHP\Async\TimeoutException がスローされます。
すべての Promise
oxphp_async_await_all() はすべての Promise を待機し、Promise ID をキーとする連想配列を返します。
<?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() は配列の順序どおりに Promise を順次待機します。すべてのクロージャは非同期プール上で並行して実行されますが、呼び出し側のスレッドは結果を 1 つずつ収集します。
最初に確定した Promise(race)
oxphp_async_await_race() は、いずれか 1 つの Promise が確定した時点で、成功(fulfilled)か失敗(rejected)かにかかわらず結果を返します。
<?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']}";勝者とならなかった Promise は、oxphp_async_await_race() が返った後も個別に待機可能なままです。勝者となった Promise が失敗した場合は OxPHP\Async\AsyncException がスローされますが、敗者は引き続き待機可能です。これは JavaScript の Promise.race に相当します。
最初に成功した Promise
oxphp_async_await_any() は、いずれか 1 つの Promise が成功(FULFILL)した時点で結果を返します。失敗は蓄積され、すべての Promise が失敗した場合にのみ観測可能になります。これは JavaScript の Promise.any に相当し、フォールバックや冗長性のパターン(「応答する任意のミラー」)に役立ちます。
<?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.
}勝敗が決した時点でまだ保留中だった、勝者とならなかった Promise は、個別に待機可能なままです。勝者が決まる前にすでに失敗していた Promise はそうではありません。それらの結果は、候補となるエラーとして蓄積された時点で消費されています。
例外の種類
| クラス | スロー元 | 補足 |
|---|---|---|
OxPHP\Async\AsyncException |
oxphp_async_await()、oxphp_async_await_all()、oxphp_async_await_race() |
メッセージと、任意で元の例外の詳細を含む単一のエラー。 |
OxPHP\Async\TimeoutException |
4 つの await-* すべて(期限到達時) | AsyncException を継承します。oxphp_async_await_any() のタイムアウトでは getPartialErrors() と getCancelledPromiseIds() が値を持ちます。それ以外の呼び出し箇所では両方とも [] を返します。 |
OxPHP\Async\AggregateAsyncException |
oxphp_async_await_any()(すべての Promise が失敗した場合) |
AsyncException を継承します。getErrors()(位置ベース、0..N-1 をキーとする)、getErrorMap()(ID をキーとする)、getPromiseIds() を提供します。 |
エラー処理
非同期クロージャの内部でスローされた例外は捕捉され、待機時に 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() と die() も捕捉され、OxPHP\Async\AsyncException に変換されます。非同期ワーカーは生き残り、新しいタスクの処理を継続します。
例外の階層
\Exception
└── OxPHP\Async\AsyncException # All async errors
├── OxPHP\Async\TimeoutException # Timeout-specific
└── OxPHP\Async\AggregateAsyncException # Multiple failures (await_all / await_any)ファイバーの統合
ワーカーモードでは、oxphp_async_await() は OxPHP のファイバースケジューラと協調します。ワーカースレッドをブロックする代わりに、現在のファイバーが結果を待つ間だけ中断します。スケジューラは結果が準備できると再開するため、他のリクエストは同じスレッド上で処理を進め続けます。
従来モード(ワーカーファイルなし)では、oxphp_async_await() はワーカースレッドを同期的にブロックします。ワーカーは待機中に他のリクエストを処理できません。
最良のパフォーマンスを得るには、非同期 Promise とワーカーモードを組み合わせてください。
<?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);
});コンポジション(ネストした非同期)
非同期タスク自身が oxphp_async() を呼び出して結果を待機することもできます。各タスクはスケジューラファイバーの内部で実行されるため、ネストした Promise を待機するとそのタスクのファイバーが中断され、そのワーカーが解放されてネストしたタスクを実行できるようになります。つまり、タスクは待機中にワーカーを保持することなく、子タスクへファンアウトできます。
<?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()、oxphp_async_await_any() も同様に、タスクファイバーの内部から呼び出せます。同時に存在するタスクファイバーの数(キュー内 + 実行中)は ASYNC_MAX_FIBERS × ASYNC_WORKERS で制限されます。上限を超えるディスパッチは、ブロックする代わりに OxPHP\Async\AsyncException とともに即座に拒否されるため、ファンアウトが自身の保持する容量を待機してデッドロックに陥ることはありません。
制限事項
非同期クロージャは別のスレッド上で実行されます。このため、スレッド境界を越えられるデータには制限があります。
| 許可されるもの | 許可されないもの |
|---|---|
null、bool、int、float、string |
プレーンなオブジェクト(OxPHP\Shared\Shareable を実装していない任意のクラス) |
| スカラー型の配列 | リソース(ファイルハンドル、DB 接続、ストリーム) |
| ネストしたスカラー配列 | use が非 Shareable なオブジェクトをキャプチャするクロージャ |
Shared\* のインスタンス(Counter、Map、Channel、Atomic、Flag、Mutex、Once、Pool、Registry)および OxPHP\Shared\Shareable を実装するその他のクラス |
追加の制約:
- ユーザー関数のみ — クロージャはユーザー定義でなければならず、組み込み関数をラップしたものであってはなりません
- シリアライズのオーバーヘッド — 引数と戻り値はスレッド境界を越える際にシリアライズされます。大きな配列や文字列はレイテンシを増加させます
- プレーンな PHP 値には共有状態がない — 各非同期ワーカーは独自の PHP 環境を持ちます。プレーンな変数、配列、クラスインスタンスは境界を越える際にコピーされます(または拒否されます)。両方のスレッドから見える参照を渡すには、共有状態プリミティブ(
Shared\Counter、Shared\Map、Shared\Channelなど)を使用してください
Docker の例
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=256トラブルシューティング
"Async pool is disabled. Set ASYNC_WORKERS > 0 to enable."
非同期プールが設定されていません。ASYNC_WORKERS=0(デフォルト)の場合、非同期関数は登録されていますが、呼び出しのたびに OxPHP\Async\AsyncException をスローします。
修正: ASYNC_WORKERS を正の値に設定します。
ASYNC_WORKERS=4"Failed to dispatch async task (pool full)"
非同期プールは稼働していますが、キューのスロットがすべて埋まっているか、プロセス全体の処理中(in-flight)タスクの上限(ASYNC_MAX_FIBERS × ASYNC_WORKERS、キュー内 + 実行中のタスクを含む)に達しています。いずれの場合もディスパッチ時に OxPHP\Async\AsyncException をスローし、oxphp_async_tasks_rejected_total を増加させます。
確認: プールがタスクを受け付けているか確認します。
curl -s http://localhost:9090/config | jq '.async_workers'修正: ASYNC_WORKERS または ASYNC_QUEUE_CAPACITY を増やします。
"Cannot pass object values in use-vars to async closure"
オブジェクトはスレッド境界を越えてシリアライズできません。
修正: ディスパッチする前に、必要なスカラーデータを抽出します。
<?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) { ... });従来モードで待機がハングする
従来モードでは、oxphp_async_await() はワーカースレッドをブロックします。すべての PHP ワーカーが非同期結果を待ってブロックされると、サーバーはリクエストの処理を停止します。
修正: ワーカーモード(WORKER_MODE_ENABLED=true)を有効にして、oxphp_async_await() がスレッドをブロックする代わりにファイバーを中断するようにします。
タイムアウトは放棄されたタスクをキャンセルする
OxPHP\Async\TimeoutException は、期限が過ぎた瞬間に待機側で発生します。バックグラウンドタスクは、もはや監視されないまま実行され続けることはありません。oxphp_sleep() でパークされているタスクや子 Promise を待機して中断されているタスクは再開されて巻き戻され(その finally ブロックが実行され)、譲らない CPU バウンドなタスクはオペコード境界で中断されます。したがってキャンセルはベストエフォートであり、短いレイテンシの上限を伴い、即時ではありません。同じキャンセルは、oxphp_async_await_all() が放棄する Promise や、oxphp_async_await_race() / oxphp_async_await_any() の敗者にも適用されます。それでも時間内に中断できないタスクはストランド(stranded) — キャンセルされたものの、リクエストの終了時に排出されます。これにより RSHUTDOWN が最大で数秒延びることがあります。oxphp_async_tasks_stranded_total を監視してください。
ベストプラクティス
- 常にタイムアウトを設定する — 本番環境では
oxphp_async_await()の呼び出しにタイムアウトを設定し、無期限の待機を防ぎます - ワーカーモードを使用する — ワーカースレッドをブロックする代わりに、ファイバーベースのノンブロッキングな待機を実現します
- クロージャを小さく保つ — リクエストハンドラ全体ではなく、焦点を絞った作業単位をディスパッチします
- ディスパッチ前にスカラーを抽出する — クロージャに渡す前に、ID、文字列、設定値をオブジェクトから取り出します
- 非同期プールを監視する — Prometheus メトリクスの
oxphp_async_tasks_rejected_totalを確認します。拒否が増加している場合は、ASYNC_WORKERSまたはASYNC_QUEUE_CAPACITYを増やします