ファイバー多重化

OxPHP は PHP Fibers を利用して、単一のワーカースレッド上で複数の HTTP リクエストを並行処理します。リクエストが oxphp_sleep() または oxphp_async_await()(非同期プールが有効な場合)を呼び出すと、そのリクエストは中断され、ワーカースレッドはただちに次のリクエストを処理し始めます。1 つのワーカーで、追加のスレッドを使わずに数百の処理中リクエストを管理できます。

仕組み

スケジューラーは 1 つのスレッド上で多数のリクエストを実行し、それぞれに専用のファイバーを割り当て、ファイバーが中断するたびに切り替えを行います。

  1. リクエストが到着すると、スケジューラーはそれをファイバーに割り当てます。ファイバーとは、独自のスタックと PHP の状態を持つ軽量な実行コンテキストです。
  2. ファイバーは oxphp_worker() ハンドラーを実行します。ハンドラーが中断せずに完了した場合はレスポンスが送信され、ファイバーは単一リクエストのワーカーと比べてオーバーヘッドゼロで再利用されます。
  3. ハンドラーが中断する関数(oxphp_sleep()oxphp_usleep()oxphp_async_await())を呼び出すと、ファイバーはスケジューラーに制御を返します。
  4. スケジューラーは新たに到着したリクエストを取り上げ(新しいファイバーを生成)、待機条件が満たされた(タイマーの期限切れ、非同期結果の準備完了)中断中のファイバーを再開します。
  5. 各ファイバーの PHP の状態(スーパーグローバル、レスポンスヘッダー、出力バッファ、VM スタック)は中断時に保存され、再開時に復元されます。ファイバーは互いに完全に分離されています。
graph LR
  W["Worker Thread"]
  W --> A0["Fiber A: handling /api/users"]
  A0 --> A1["oxphp_sleep(0.5)"]
  A1 --> A2["suspended"]
  A2 --> A3["resumed"]
  A3 --> A4["response"]
  W --> B0["Fiber B: handling /api/orders"]
  B0 --> B1["oxphp_async_await($p)"]
  B1 --> B2["suspended"]
  B2 --> B3["resumed"]
  B3 --> B4["response"]
  W --> C0["Fiber C: handling /health"]
  C0 --> C1["response (no suspension, zero overhead)"]

設定

ファイバー多重化はワーカーモードが有効なときに自動的に作動します。追加の環境変数はありません。

Variable Default Description
WORKER_MODE_ENABLED false true に設定し、ENTRY_FILE.php のブートストラップに向けると、ワーカーモードとファイバー多重化が有効になります
PHP_WORKERS CPU / 2 (min 1) ワーカースレッドの数。各スレッドは最大 256 の並行ファイバーを持つ独立したスケジューラーを実行します

ワーカースレッドあたりの並行ファイバーの最大数は 256 です。4 つのワーカースレッドがあれば、OxPHP は最大 1,024 の処理中リクエストを同時に処理できます。

中断ポイント

以下の関数は現在のファイバーを中断し、同じスレッド上で他のリクエストを実行できるようにします。

Function What happens
oxphp_sleep(float $seconds) 指定した時間だけファイバーを中断します。他のファイバーは実行を続けます
oxphp_usleep(int $microseconds) oxphp_sleep() と同じですが、マイクロ秒単位の粒度です(最小 1 ms)
oxphp_async_await(int $promise_id) バックグラウンドスレッドプール上で非同期タスクが完了するまでファイバーを中断します

以下の関数はファイバーを中断しません

Function Behavior
oxphp_stream_flush() チャンクをただちにクライアントへ送信して返ります。SSE ループ内で oxphp_sleep() と併用してください
oxphp_finish_request() 完全なレスポンスを送信し、PHP の実行を継続します。制御を返しません
Note

PHP 組み込みの sleep()usleep() はワーカースレッド全体をブロックします。協調的な動作を得るには、必ず oxphp_sleep()oxphp_usleep() を使用してください。

PHP の例

基本的な並行処理

中断しないリクエストは、ファイバーのオーバーヘッドなしにフルスピードで実行されます。

worker.php
<?php oxphp_worker(function () { $path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); if ($path === '/health') { echo json_encode(['status' => 'ok']); return; // No suspension — runs at full speed } if ($path === '/slow') { oxphp_sleep(2.0); // Yields for 2 seconds — other requests run echo "Done after 2s delay"; return; } echo "Hello"; });

ノンブロッキングな API 呼び出し

oxphp_async()oxphp_async_await() を組み合わせると、ワーカーをブロックすることなく外部 API を呼び出せます。

worker.php
<?php oxphp_worker(function () { // Dispatch two API calls to the async thread pool $p1 = oxphp_async(fn() => file_get_contents('https://api.example.com/users')); $p2 = oxphp_async(fn() => file_get_contents('https://api.example.com/orders')); // Await both — the fiber suspends, other requests run on this thread $users = oxphp_async_await($p1); $orders = oxphp_async_await($p2); header('Content-Type: application/json'); echo json_encode(['users' => json_decode($users), 'orders' => json_decode($orders)]); });

協調的スリープを用いた SSE

oxphp_stream_flush()oxphp_sleep() を使って、Server-Sent Events を他のリクエストと交互に処理します。

worker.php
<?php oxphp_worker(function () { header('Content-Type: text/event-stream'); header('Cache-Control: no-cache'); for ($i = 0; $i < 30; $i++) { echo "data: " . json_encode(['count' => $i, 'time' => time()]) . "\n\n"; oxphp_stream_flush(); // Send chunk now (does not suspend) oxphp_sleep(1.0); // Yield for 1 second (other requests run) } });

ブロッキング I/O

ファイバー多重化は協調的であって、プリエンプティブではありません。ブロッキング関数を呼び出したファイバーは、ワーカースレッド全体をフリーズさせます。そのスレッド上の他のファイバーはいずれも処理を進められません。

ワーカーをブロックする関数

  • file_get_contents()fopen()fread()
  • curl_exec()curl_multi_exec()
  • PDO クエリ、mysqli_query()
  • PHP の sleep()usleep()(代わりに oxphp_sleep() を使用)
  • DNS 解決(gethostbyname()
  • 同期的なネットワークまたはディスク I/O 全般

ブロッキングを避ける方法

ブロッキング処理を oxphp_async() でラップし、非同期スレッドプール上で実行します。

php
<?php // WRONG — blocks the entire worker thread $html = file_get_contents('https://example.com'); // CORRECT — runs on async pool, fiber yields $promise = oxphp_async(fn() => file_get_contents('https://example.com')); $html = oxphp_async_await($promise);

データベースクエリの場合は次のとおりです。

php
<?php $db = new PDO('mysql:host=db;dbname=app', 'root', 'secret'); // WRONG — blocks the worker $users = $db->query('SELECT * FROM users WHERE active = 1')->fetchAll(); // CORRECT — query runs on async thread, fiber yields $promise = oxphp_async(function () { $db = new PDO('mysql:host=db;dbname=app', 'root', 'secret'); return $db->query('SELECT * FROM users WHERE active = 1')->fetchAll(); }); $users = oxphp_async_await($promise);
Note

オブジェクトはスレッド間でシリアライズできないため、データベース接続を oxphp_async() に渡すことはできません。非同期クロージャの内部で接続を作成するか、クエリが十分に速くブロッキングが許容できる場合はファイバー内で直接クエリを実行してください。

Warning

oxphp_async() には ASYNC_WORKERS > 0 が必要です。非同期プールが無効な場合(デフォルト)、oxphp_async() を呼び出すと OxPHP\Async\AsyncException がスローされます。

ファイバーの再利用の仕組み

ファイバーの C スタックは一度だけ確保され、複数のリクエストにわたって再利用されます。ファイバーがリクエストの処理を終えても破棄されず、スケジューラーへ中断して戻り、フリーリストに追加されます。次のリクエストは既存の C スタックを再利用するため、コストのかかるメモリ確保を回避できます。

PHP の VM スタック(関数呼び出しフレームに使用)は、リクエストごとに新しく確保され、ハンドラーが返るときに解放されます。

Docker の例

compose.yaml
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 - PHP_WORKERS=4 - ASYNC_WORKERS=8

この設定では、4 つのワーカースレッドそれぞれが最大 256 の並行ファイバーを処理でき、ブロッキング I/O は 8 つの非同期ワーカースレッドにオフロードされます。

トラブルシューティング

あるリクエストが重い I/O を行うとリクエストが遅くなる

あるファイバーが oxphp_async() を使わずにブロッキング関数(データベースクエリ、HTTP リクエスト、ファイル読み込み)を呼び出しています。これはワーカースレッド全体をブロックします。

対処: ブロッキング呼び出しを oxphp_async() でラップしてください。

php
<?php $promise = oxphp_async(fn() => file_get_contents($url)); $result = oxphp_async_await($promise);
"Async pool is disabled. Set ASYNC_WORKERS > 0 to enable."

非同期プールが設定されていません。ASYNC_WORKERS=0(デフォルト)の場合、すべての非同期関数は OxPHP\Async\AsyncException をスローします。

対処: ASYNC_WORKERS を正の値に設定してください。

bash
ASYNC_WORKERS=8
"Failed to dispatch async task" when using oxphp_async()

非同期プールは稼働していますが、容量が上限に達しています。

対処: ASYNC_WORKERS または ASYNC_QUEUE_CAPACITY を増やしてください。

bash
ASYNC_WORKERS=8 ASYNC_QUEUE_CAPACITY=512
oxphp_sleep() が他のリクエストに制御を渡さない

ファイバー多重化はワーカーモードでのみ機能します。従来のモードでは、oxphp_sleep() はブロッキングな usleep() にフォールバックします。

対処: WORKER_MODE_ENABLED=true を設定してワーカーモードを有効にしてください。

並行リクエストが多いときにメモリ使用量が高くなる

各ファイバーは C スタック(デフォルト 8 MiB、PHP の fiber.stack_size ini 設定で構成)に加えて、リクエストごとに PHP の VM スタックを使用します。256 の並行ファイバーがある場合、最悪ケースの C スタックのメモリはワーカースレッドあたり 2 GiB になります。

対処: アプリケーションが深い再帰を使用しない場合は、php.inifiber.stack_size を減らしてください。

php.ini
fiber.stack_size = 512K

制限事項

  • ワーカーモード限定 — ファイバー多重化は従来のモードでは利用できません
  • ワーカーあたり 256 ファイバー — ハードリミットであり、実行時に構成することはできません
  • 協調的のみ — CPU バウンドなコード(タイトなループ、重い計算)は他のファイバーを飢餓状態にします。プリエンプションはありません
  • ブロッキング I/O はスレッドをブロックする — 真の並行性を得るには、すべてのブロッキング呼び出しを oxphp_async() でラップする必要があります
  • PHP ネイティブの sleep()/usleep() はファイバーを認識しないoxphp_sleep()/oxphp_usleep() を使用してください
  • oxphp_async_await_race()oxphp_async_await_any() は制御を返さない — これらは現在、ファイバー内であってもブロックします。oxphp_async_await_all() は待機中にファイバーを中断するため、ファイバーフレンドリーです。race/any については、スレッドを協調的に保つ必要がある場合は逐次的な oxphp_async_await() 呼び出しを使用してください

関連項目

  • ワーカーモード — 永続的な PHP プロセスと oxphp_worker() API
  • Async Promises — ブロッキング I/O をオフロードするためのバックグラウンドスレッドプール
  • SSE — ファイバーベースの協調的スリープと組み合わせたリアルタイムストリーミング
  • PHP 関数oxphp_sleep()oxphp_usleep()、その他のファイバー対応関数
  • 設定リファレンスWORKER_MODE_ENABLEDENTRY_FILEPHP_WORKERSASYNC_WORKERS