こんにちは。普段、Node.jsやGo、あるいはRustといった「非同期・並行処理が当たり前」の言語からPHPの世界を眺めていて、「PHPでリアルタイム性の高いWebSocketサーバーやストリーミング処理をやるのは、どうせプロセスが重いし無理なんだろうな」なんて壁にぶつかっていませんか?
もし、あなたがそう感じているなら、それはPHP 8.1以前の古い常識に縛られているだけかもしれません。
今回は、PHP 8.1で導入されたFiber(ファイバー)を主役に据え、イベントループと組み合わせることで、PHPのプロセスモデルを限界まで効率化し、数千のWebSocketコネクションを軽々とさばくメッセージングアーキテクチャの裏側を紐解いていきます。
ここを理解すると、「なぜPHPがWebの歴史を生き抜いてきたのか」、そして「モダンなPHPエンジンがどれほど洗練されているか」が綺麗に見えてきますよ。それでは、深淵なるZend VMの世界へ少しだけ足を踏み入れてみましょう。
—
1. PHPにおける「非同期」のパラダイムシフト:なぜFiberなのか?
これまでのPHP(特に一般的なFPM環境)は、「1リクエスト=1プロセス(または1スレッド)」の同期型モデルが基本でした。クライアントからリクエストが来ると、Zendエンジンがスクリプトを解釈し、DBや外部APIへのI/O(ネットワーク待ち)が発生するたびにプロセス全体がブロックされます。
これでは、数百・数千の常時接続を維持するWebSocketサーバーを構築しようものなら、OSのスレッドやプロセスが枯渇してしまいますよね。
コルーチン(協調的マルチタスク)という解決策
Node.jsは「イベントループ+コールバック(またはPromise)」でこれを解決しました。しかし、コールバック地獄やコードの可読性の低下という代償を払いました。
そこでPHP 8.1が導入したのが Fiber です。
Fiberは、「スタックを持つ協調的マルチタスク(Coroutine)」をPHPのコアに実装したものです。これにより、開発者は複雑なコールバックを書くことなく、「任意の場所で処理を中断(Suspend)し、後から再開(Resume)する」という非同期フローを、極めて自然な同期コードの見た目のままで記述できるようになりました。
—
2. 内部構造の覗き見:Zend VMとFiberのメモリ空間
少しだけ低レイヤの話をしましょう。
通常のPHP関数呼び出しは、コールスタック(Zend Execution Stack)を直線的に積み上げていきます。関数がネストすればするほどスタックフレームが深くなり、returnされると巻き戻されます。
しかし、Fiberが生成されると、PHPのCレベル(Zend VM)で独自の独立したコールスタックがヒープ上に割り当てられます。
[メインのコールスタック] <--- イベントループが稼働 | +---- Fiber::suspend() で一度処理を抜ける | v [Fiber内のコールスタック] <--- 非同期タスク(WebSocketメッセージ処理など)
- ローカル変数
- 実行ポインタ(opline)
- 制御コンテキスト
つまり、I/O待ち(例えば、ソケットからのデータ読み込みやメッセージキューのポーリング)に直面したとき、Fiberは現在の実行コンテキスト(レジスタやコールスタックの状態)を保持したまま、親のイベントループへ制御を返します(Suspend)。イベントループはその間に別のクライアントの処理を進め、I/Oの準備ができたら再びFiberを再開(Resume)させます。
プロセスを増やさず、1つのプロセス・スレッド上で時分割にタスクを切り替える——これが、PHPで実現する非同期並行処理の正体です。
—
3. 実践:FiberとイベントループによるWebSocketメッセージング
理屈はこのあたりにして、実際にコードを見てみましょう。
ここでは、外部のイベントループライブラリ(ReactPHPやAmpなどの概念に近いシンプルな実装)を想定し、WebSocketのメッセージキューイングとFiberスケジューリングを行うコアロジックを構築します。
/
class SimpleEventLoop
{
private \SplQueue $tasks;
public function __construct()
{
$this->tasks = new \SplQueue();
}
// タスク(コールバック)をキューに追加
public function addDeferred(callable $task): void
{
$this->tasks->enqueue($task);
}
// イベントループの駆動
public function run(): void
{
while (!$this->tasks->isEmpty()) {
$task = $this->tasks->dequeue();
// タスクを実行
$task();
}
}
}
/
- WebSocketのクライアント接続とメッセージキューを管理するマネージャー
/
class WebSocketServerManager
{
private SimpleEventLoop $loop;
private array $messageQueue = [];
public function __construct(SimpleEventLoop $loop)
{
$this->loop = $loop;
}
/
- クライアントからのメッセージを非同期に処理する(Fiberを使用)
/
public function handleClientMessage(string $clientId, string $payload): void
{
// 各クライアントのメッセージ処理をFiberとして独立させる
$fiber = new \Fiber(function (string $id, string $data) {
echo “[Fiber] クライアント {$id} からのメッセージ解析を開始…\n”;
// 重いJSONパースやバリデーションをシミュレート
\Fiber::suspend(‘parse_wait’);
$decoded = json_decode($data, true, 512, JSON_THROW_ON_ERROR);
echo “[Fiber] データベースへの非同期書き込みをシミュレート…\n”;
// DBへのI/O待ちをシミュレート(ここで処理を一時中断)
\Fiber::suspend(‘db_io_wait’);
// キューへメッセージをブロードキャスト用に積む
$this->messageQueue[] = [
‘sender’ => $id,
‘message’ => $decoded[‘text’] ?? ”,
‘time’ => time(),
];
echo “[Fiber] クライアント {$id} の処理が完了しました。\n”;
return “OK: {$id}”;
});
// Fiberを開始(最初のsuspendまで実行される)
$status = $fiber->start($clientId, $payload);
$this->processFiberState($fiber, $status);
}
/
- Fiberの状態(Suspendされた理由)に応じてイベントループに再開タスクを登録する
/
private function processFiberState(\Fiber $fiber, mixed $suspendReason): void
{
if ($fiber->isTerminated()) {
return;
}
// I/O待ちや重い処理のシミュレーション(タイマーや非同期ソケットの代わりに遅延実行)
$this->loop->addDeferred(function () use ($fiber, $suspendReason) {
echo “[EventLoop] 理由 ‘{$suspendReason}’ の待機が完了しました。Fiberを再開します。\n”;
// 処理を再開し、次のsuspendまたは終了まで進める
try {
$status = $fiber->resume();
if (!$fiber->isTerminated()) {
$this->processFiberState($fiber, $status);
}
} catch (\Throwable $e) {
echo “[Error] Fiber内で例外が発生: ” . $e->getMessage() . “\n”;
}
});
}
public function getQueue(): array
{
return $this->messageQueue;
}
}
// ==========================================
// 実行シミュレーション
// ==========================================
$loop = new SimpleEventLoop();
$server = new WebSocketServerManager($loop);
echo “=== WebSocketサーバーのモック起動 ===\n”;
// クライアントAからのメッセージ受信
$server->handleClientMessage(“client_A”, json_encode([“text” => “こんにちは、PHP Fiber!”]));
// クライアントBからのメッセージ受信(イベントループ上で並行して扱われる)
$server->handleClientMessage(“client_B”, json_encode([“text” => “非同期処理のテストです。”]));
// イベントループを駆動させる
$loop->run();
echo “=== すべての処理が完了しました ===\n”;
—
4. このコードが教えてくれること:脳内トレースのポイント
上記のスクリプトを実行すると、コンソールには次のような順序でログが出力されます。
1. クライアントAのFiberが起動し、最初の `suspend(‘parse_wait’)` まで走って止まる。
2. クライアントBのFiberが起動し、同じく最初の `suspend(‘parse_wait’)` まで走って止まる。
3. イベントループが回り始め、Aのパース待ちが解除されて再開、次にDB I/O待ちで止まる。
4. Bのパース待ちが解除され、同様に処理が進む。
ここでのポイントは、「クライアントAのDB I/Oが終わるのを待っている間に、クライアントBのメッセージ解析が進んでいる」という点です。従来のPHPスクリプトであれば、AのDB書き込みが終わるまでBの処理は完全にブロックされていました。しかし、Fiberを使うことで、1つのプロセス内であたかもマルチスレッドのように処理がインターリーブ(交差)しています。
アーキテクトからの実践的なアドバイス
実際のプロダクション環境(例えば、Swoole, OpenSwoole, あるいは RevoltをベースにしたRatchetなど)では、このイベントループやソケットの監視はC拡張や底层の非同期エンジンが極めて高速に処理してくれます。
私たちが書くべきアプリケーションコードは、このFiberの仕組みを前提とした「ブロッキングしない設計」を守ることです。コード内で安易に重い同期ブロッキング関数(例えば、従来の `sleep()` や、タイムアウトの長い同期HTTPクライアント)を呼んでしまうと、その瞬間に対象のFiberだけでなく、イベントループ全体の足を引っ張ることになります(これを「イベントループのブロック」と呼びます)。
非同期の世界では、「待つときは、自ら進んで `suspend` し、準備ができたら `resume` してもらう」。この協調の精神を忘れないでください。
—
まとめ
PHPにおけるFiberの導入は、単なる「新しい文法の追加」ではありません。それは、PHPという言語が長年抱えてきた「I/Oバウンドな処理への弱さ」というアーキテクチャ上の制約を、モダンなコルーチンモデルによって鮮やかに克服するための強力な武器です。
「PHPは遅い、非同期が苦手」という先入観は、もう古いものになりつつあります。内部のZend VMがどうメモリを管理し、イベントループとFiberがどう協調しているのかを知れば、あなたの書くPHPコードは、他のどの言語にも劣らない洗練された非同期Webシステムへと生まれ変わるはずです。
さあ、次のプロダクトでは、ぜひFiberを活用したリアルタイムストリーミングに挑戦してみてください。きっと、PHPの新しい可能性に驚くはずですよ。