ファイバー多重化

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 の状態($_GET、$_POST、$_COOKIE、$_SERVER、$_FILES、$_REQUEST、レスポンスヘッダー、出力バッファ、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)"]
ファイバーごとに退避されないもの

上の一覧にないものは、同じスレッド上で重なり合って実行されるリクエスト間で共有されます。名前を挙げて覚えておくべきなのはセッションです。$_SESSION、セッション ID、そしてその背後にある開いたセッションは、ワーカースレッドごとに 1 組しかありません。セッションを開いたまま中断したリクエストの横でスケジューラーが受け入れたリクエストには、自分のセッションではなくそのセッションが渡され、session_write_close() を呼んでも両者は分離されません(ワーカーモード → セッションの状態を参照)。$_ENV はさらに踏み込んでいます。これは設計上ワーカーの状態であり、あるリクエストがそこに書き込んだ内容は、重なり合っているかどうかに関係なく、そのワーカー上の後続のすべてのリクエストから読み取られます(スーパーグローバルを参照)。

設定

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

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) バックグラウンドスレッドプール上で非同期タスクが完了するまでファイバーを中断します。oxphp_sleep() と同じコンテキストでは、代わりにワーカースレッド上で待機します

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

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

PHP 組み込みの sleep() と usleep() は、デフォルトではワーカースレッド全体をブロックします。oxphp_sleep() / oxphp_usleep() を使用するか、RUNTIME_HOOKS=sleep を設定してネイティブの組み込み関数もファイバーを中断するようにしてください。後者はサードパーティのコードが sleep() を直接呼び出す場合に有用です。設定を参照してください。

何も中断しない場所

oxphp_* 関数もフックされた組み込み関数も、ファイバーの切り替えが可能な場所でしか中断しません。切り替えができない場所では、代わりにワーカースレッド上で待機します。フックされた組み込み関数は呼び出しをネイティブの sleep()/usleep() に引き渡すため、その待機も、シグナルで中断されたときに sleep() が返す値も、組み込み関数のものになります。該当するのは、declare(ticks) ハンドラー、pcntl のシグナルディスパッチ、リクエストの入力の構築、filter_input_array() からの FILTER_CALLBACK、そしてリクエストが解体された後に実行されるデストラクターです。この一覧は網羅的なものではなく、全体はランタイムフックに記載しています。

アプリケーション自身が開始したファイバー(new Fiber によるもの、またはアプリケーションが駆動するスケジューラー経由のもの)は別のケースです。OxPHP は自分が所有していないコンテキストを再開できないため、その中でのスリープはワーカースレッドを占有します。負荷のうちこの部分は、フックが無効であるものとして見積もってください。

サーバーが設定を読み込んだことを確認するには、oxphp_server_info() または内部サーバーの /config から runtime_hooks を読み取ってください。認識されない環境変数は、どこでもエラーになりません。

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() を使用するか、RUNTIME_HOOKS=sleep を有効化。ただし中断ポイントに記載した制限の範囲内)
  • DNS 解決(gethostbyname())
  • 同期的なネットワークまたはディスク I/O 全般

ソケットの読み取りと stream_select() は、RUNTIME_HOOKS=streams を有効にすると例外になります。どちらもスレッドではなくファイバーを中断するため、PHP ストリームの読み取りでブロックするクライアント(mysqlnd、phpredis、HTTP ストリームラッパー)だけでなく、複数のソケットを同時に待つループもカバーされます。streams だけの場合、書き込み、socket_select()、ext/curl、接続の確立、DNS 解決は引き続きブロックします。net カテゴリーはさらに、接続の確立(永続接続を除く)、TLS のハンドシェイクと読み取り、unix://・udp://・udg:// ソケット、ソケットペア、TCP 以外のソケットからエクスポートされたストリームでの読み取り、そして空きを待つ書き込みも中断しますが、socket_select()、ext/curl、DNS 解決は引き続きブロックします。カテゴリーがそもそもインストールされたかどうかは、oxphp_server_info() と /config が runtime_hooks として報告します。設定を参照してください。

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

ブロッキング処理を 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 スタック(関数呼び出しフレームに使用)も同じように保持されます。各ファイバーの VM スタックはそのファイバーが最初に起動したときに確保され、そのファイバーが処理するすべてのリクエストで再利用されます。呼び出しチェーンがそれを超えて伸びたリクエストには追加のページが割り当てられ、それらの呼び出しが終わると返却されます。

Docker の例

compose.yaml
services: app: image: ghcr.io/oxphp/oxphp:0.13.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 を設定してワーカーモードを有効にしてください。

ワーカーモードがすでに有効な場合、その呼び出しはファイバーの切り替えが拒否される場所か、OxPHP がコンテキストを所有していない場所にあります。これらは中断ポイントにまとめています。そこに行き着くよくある経路は、アプリケーション自身が開始したファイバー(new Fiber によるもの、またはリクエスト内でアプリケーションが駆動するスケジューラー経由のもの)であり、どの設定でもこれは変わりません。

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

各ファイバーは、PHP の fiber.stack_size ini 設定でサイズが決まる専用の C スタック(設定が未指定の場合、64 ビットプラットフォームでは 2 MiB)と、専用の PHP の VM スタックの上で動作します。ワーカースレッドあたり 256 ファイバーという上限では、スレッドあたり 512 MiB の C スタックになりますが、これは使用中のメモリではなく、オペレーティングシステムからマップされたアドレス空間です。ファイバーのスタックのページは、そのファイバー上で動くコードがそこに到達して初めて常駐します。ワーカーは作成したすべてのファイバーをスタックごと終了するまで保持し、後続のリクエストに再利用します。そのため、かつて深く再帰したリクエストを処理したファイバーは、ワーカーが生きている限りそれらのページを保持し続けます。

対処: 選んだサイズより深い C スタックを必要とするリクエストがない場合は、php.ini の fiber.stack_size を下げて、1 つのファイバーが保持できるスタックの量に上限を設けてください。

php.ini
fiber.stack_size = 512K

これによりアドレス空間も縮小し、ファイバーの上限ではワーカースレッドあたり 128 MiB になります。常駐メモリが減るのは、リクエストが新しいサイズより深くまで進んでいたはずの場合だけで、そのリクエストは代わりに失敗するようになります。それより深くなる呼び出しは Error: Maximum call stack size of … bytes … reached をスローします。

制限事項

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

エージェント向けプロンプト

これらのプロンプトは、OxPHP ドキュメント MCP サーバー を接続したエージェントを前提としています — 必要なページはエージェントが自分で読み取ります。

prompt
私のリクエストハンドラーを OxPHP の協調的スケジューラーの下で安全に動くようにしてください。OxPHP ドキュメント MCP サーバー経由で `features/fiber-multiplexing` ページを読んでください。 このリポジトリを一通り調べて、ファイバーを中断させる代わりにワーカースレッドをブロックしてしまう呼び出しを洗い出し、非同期プールへ移すべきものはどれか、すでにランタイムフックがカバーしているものはどれか、十分に短いのでそのままにしてよいものはどれかを教えてください。

関連項目

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