こんにちは。普段、Node.jsやGo、あるいはRustなどで「非同期処理」や「イベントループ」をバリバリ書きこなしているあなたなら、PHPの歴史的な「同期・ブロッキング」というイメージに対して、どこかでもどかしさを感じたことがあるかもしれませんね。
「PHPだって、モダンな非同期並行処理ができるんだ」
そう聞いて、ReactPHPやAmpといったユーザーランドのイベントループライブラリを思い浮かべる方も多いでしょう。しかし、PHP 8.1で導入された `Fiber`(ファイバー) は、それらとは根本的に異なります。ライブラリの力技ではなく、Zendエンジンそのものがネイティブで「実行コンテキストの切り替え」をサポートした という、PHPの歴史における最大のパラダイムシフトの一つなのです。
今回は、このFiberがPHPの内部(Zend VM)でどのようにうごめき、あの重厚長大に見えるWebリクエストの裏側でいかに華麗にコンテキストスイッチを行っているのか、そのオペコードレベルの真実を紐解いていきましょう。
ここを理解すると、PHPの裏側がまるで美しい精密機械のように綺麗に見えてきますよ。一緒に覗いてみましょう。
—
1. そもそもFiberとは何か? 〜スタックless vs スタックfullの誤解を解く〜
他の言語(例えばGoのGoroutineなど)を触ったことがある人ほど、「PHPのFiberは軽量スレッドなんだな」と誤解しがちです。しかし、PHPのFiberは正確には「スタックフル・コルーチン(Stackful Coroutine)」であり、OSスレッドとは全く別物です。
OSスレッドはカーネルがスケジューリングしますが、Fiberは完全にユーザーランド(PHPスクリプトの制御下)でスケジューリングされます。
従来のPHPでは、関数を呼び出すとZend VMはコールスタック(実行コンスタントを積むスタックフレーム)を深く積んでいき、returnするまでその場から動けませんでした。I/O待ち(DBクエリやHTTPリクエスト)が発生すると、CPUはそのスレッドごとブロックされ、次のリクエストを処理できません。
これを解決するのがFiberです。Fiberを使えば、「処理の途中でいったんCPUの実行権を手放し(Suspend)、外側のイベントループ等から後で再開させる(Resume)」という離れ業が、コールスタックの途中であっても可能になります。
—
2. Zend VMの裏側:`suspend()` と `resume()` のオペコード
では、PHPのコード上で `Fiber::suspend()` が実行されたとき、Zend VMの内部では一体何が起きているのでしょうか?
PHPのコードは、そのままでは実行されません。Parserが解析し、AST(抽象構文木)を経て、Zend VMが解釈できる「オペコード(Opcode)」にコンパイルされます。
通常、関数呼び出しや制御構文の移動は、VMの実行ポインタ(`execute_data`)が直線的あるいは階層的に動きます。しかし、Fiberの操作には専用の低レイヤフックが存在します。
実行コンテキストの退避と復元
Zendエンジン内部には、現在の実行状態(ローカル変数、関数ポインタ、スタックフレーム等)を保持する `zend_execute_data` という構造体があります。
1. `Fiber::suspend()` のコール:
Zend VMは、現在のFiberが持っている `zend_execute_data` やVMスタックのポインタを、OSのヒープ上に確保されたFiber専用の退避領域へとスワップアウトします。そして、実行権をFiberを呼び出した側(親コンテキスト / メインスレッド)へと一瞬で戻します。
2. `Fiber::resume()` のコール:
逆に再開時は、退避させていた `zend_execute_data` を再びZend VMの現行ポインタにアタッチ(スワップイン)し、中断されたまさにそのオペコードの次の位置から実行を何食わぬ顔で再開します。
この仕組みにより、OSやWebサーバー(Nginx + PHP-FPMなど)のプロセスをブロックすることなく、同一プロセス・同一スレッド内で「CPUのマルチタスク」を擬似的に実現しているのです。
—
3. 実践:Fiberの基本挙動と脳内トレース
百聞は一見にしかず。実際にFiberを使いながら、頭の中でZend VMがどう動いているかをトレースしてみましょう。
{$value}\n”;
// 再び中断
$finalValue = Fiber::suspend(‘中断2:最終確認中’);
echo “[$name] Fiber内部:完了します。最終値 -> {$finalValue}\n”;
});
// — メインコンテキスト側の処理 —
echo “メイン:Fiberを起動します。\n”;
// Fiberの開始(コンテキストがFiber内部へジャンプ)
// ここでsuspendに渡された文字列 ‘中断1:データ待機中’ が返り値となる
$output1 = $fiber->start(‘開発者’);
echo “メイン:Fiberから返ってきた値 -> ‘{$output1}’\n”;
echo “メイン:少し処理を進めてから、Fiberに値を送って再開させます。\n”;
// Fiberを再開(引数 ‘りんご’ をsuspendの戻り値として渡す)
$output2 = $fiber->resume(‘りんご’);
echo “メイン:Fiberから返ってきた値 -> ‘{$output2}’\n”;
echo “メイン:Fiberに最後の値を送って完全に終了させます。\n”;
$fiber->resume(‘みかん’);
echo “メイン:すべての処理が完了しました。\n”;
このコードの実行結果
メイン:Fiberを起動します。
[開発者] Fiber内部:処理を開始しました。
メイン:Fiberから返ってきた値 -> ‘中断1:データ待機中’
メイン:5秒待つ代わりに、Fiberに値を送って再開させます。
[開発者] Fiber内部:再開しました。受け取った値 -> りんご
メイン:Fiberから返ってきた値 -> ‘中断2:最終確認中’
メイン:Fiberに最後の値を送って完全に終了させます。
[開発者] Fiber内部:完了します。最終値 -> みかん
メイン:すべての処理が完了しました。
見事に処理が「行ったり来たり」していますね。関数が普通に `return` したわけでもないのに、途中で止まって外側のコードに制御が戻り、また元の場所からスルスルと動き出す。これが、Zend VMのスタックフレーム操作の魔法です。
—
4. なぜFiber単体では「非同期Webアプリケーション」にならないのか?
ここで非常に重要な注意点、そして多くのモダン開発者がハマる罠についてお話しておきましょう。
「よし、Fiberを覚えたから、今日から俺のPHPアプリはNode.js並みに非同期で爆速になるぞ!」
……残念ながら、Fiber単体ではI/O待ちの時間を自動で短縮することはできません。
ここが、GoのGoroutine(ランタイムがネットワークI/Oを検知して自動でスケジューリングする)や、Node.jsのイベントループと決定的に違うところです。
PHPの標準関数(例えば `file_get_contents()` や `PDO::query()` など)は、同期ブロッキング関数です。これらをFiberの中で呼び出すと、結局その瞬間OSレベルでプロセス/スレッドがブロックされてしまい、他のFiberに処理を譲る(yieldする)ことができません。
真の高速化には「イベントループ」との統合が必要
PHPでFiberの真価を発揮させるには、Amp v3 や ReactPHP といった、非同期I/Oをサポートするエコシステムと組み合わせる必要があります。
これらは、内部で `stream_select()` や `ext-uv`(libuvのPHPバインディング)などのイベントループを回し、「ソケットが読み込み可能になったら、あのFiberを `resume()` する」というスケジューリングをユーザーランドで構築しています。
// 概念的なイメージ:非同期HTTPクライアントとFiberの融合
// ※実際のライブラリではもっと洗練されたAPIが提供されています
$loop = EventLoop::get();
$fiberA = new Fiber(function() use ($httpClient) {
// この中で非同期リクエストを投げ、完了を待たずにsuspendする
$response = $httpClient->asyncGet(‘https://api.example.com/data1’);
echo “データ1取得完了: {$response}\n”;
});
$fiberB = new Fiber(function() use ($httpClient) {
$response = $httpClient->asyncGet(‘https://api.example.com/data2’);
echo “データ2取得完了: {$response}\n”;
});
$fiberA->start();
$fiberB->start();
// イベントループがI/Oの完了を検知して各Fiberをresumeしていく
$loop->run();
このアーキテクチャを採用することで、1つのPHP-FPMプロセス(あるいはSwoole/FrankenPHPなどの長期常駐型サーバー)が、何百もの並行リクエストをブロッキングなしで効率よく裁くことができるようになります。
—
5. アーキテクトからのメッセージ:これからのPHP設計に向けて
PHPにおけるFiberの導入は、単なる「新しい文法が1つ増えた」という話ではありません。
Zend VMという長年培われてきた堅牢な実行エンジンの内部構造に手を入れ、「実行コンテキストの制御権を開発者に開放した」という歴史的な大改革です。
もしあなたが、従来の「1リクエスト = 1プロセス/スレッドで上から下へ同期的に実行する」というPHPの常識に縛られているなら、ぜひFiberの低レイヤの挙動を意識してみてください。
「どこでコンテキストをスイッチし、どのタイミングでイベントループに制御を返すべきか」を設計できるようになると、PHPで見違えるほどスケーラブルで美しい非同期アーキテクチャが描けるようになります。
ここを理解したあなたなら、もう「PHPはレガシーな同期言語だ」なんて言わせないはずです。ぜひ、次のプロジェクトのパフォーマンスチューニングや設計に、このFiberの知見を役立ててみてくださいね。