こんにちは。日々のコードリーディングやパフォーマンスチューニング、本当にお疲れ様です。
Node.jsやGo、あるいはRustといった他言語の非同期・並行処理モデルに触れてきた優秀なエンジニアほど、PHPのコードを書くときに「なぜ1リクエストごとにプロセス(あるいはスレッド)が閉じ、I/O待ちのたびにCPUがブロックされるのか」というもどかしさを感じたことがあるのではないでしょうか。
「PHPでも、モダンなリアクティブプログラミングやストリーム処理を美しく書きたい」
その願いを叶える鍵こそが、PHP 8.1でひそかに導入された Fiber(ファイバー) です。今回は、表面的なAPIの使い方ではなく、ZendエンジンがFiberをどう扱い、イベントループと組み合わせることで「PHPによる真のリアクティブ・フレームワーク」を構築できるのか、その核心に迫っていきましょう。
ここを理解すると、PHPの裏側が驚くほど綺麗に見えてきますよ。
—
1. なぜFiberなのか? Zend VMとスタックレス・コールバックの限界
これまでのPHP(特にReactPHPやAmpなどの第1世代非同期ライブラリ)は、イベントループと「コールバック・地獄(あるいはPromisesのチェイン)」を組み合わせることで非同期性を実現してきました。
しかし、コールバックベースのコードには致命的な問題があります。それは、コールやリターンが関数を跨ぐため、スタックトレースが断絶し、例外処理やデバッグが極めて困難になるという点です。また、ビジネスロジックが非同期の細切れの関数に分断され、人間が直感的に追える「手続き型の流れ」が破壊されてしまいます。
Fiberの本質:協調的マルチタスク(Cooperative Multitasking)
Fiberは、Zend VMの実行コンテキスト(コールスタック、変数テーブル、実行ポインタ)をユーザーランド(PHPコード側)で完全にキャプチャし、一時停止(Suspend)と再開(Resume)を自在に行えるようにする仕組みです。
OSスレッドとは異なり、OSのスケジューラではなく開発者が記述したコードの意図通りにCPUの制御権を譲り合う(協調する)ため、コンテキストスイッチのオーバーヘッドが極めて小さく、メモリ効率も抜群に高いのが特徴です。
—
2. リアクティブ・フレームワークの心臓部を作る
それでは、Fiberをベースにして、RxJSやRxPHPのような「イベントストリーム(Observable)とオペレーター」を持つ超軽量リアクティブフレームワークを自作してみましょう。
リアクティブプログラミングの核心は、「データの流れ(ストリーム)を時間軸に沿って抽象化し、非同期に流れてくる値を変形・フィルタリングする」ことにあります。
以下に、Fiberのサスペンド機構を隠蔽し、直感的な非同期ストリームを構築するコアクラスの実装を示します。
/
class Observable
{
private Closure $producer;
public function __construct(callable $producer)
{
$this->producer = Closure::fromCallable($producer);
}
/
- ストリームにオペレーターを適用して新しいObservableを返す(パイプライン構築)
/
public function pipe(callable …$operators): self
{
$stream = $this;
foreach ($operators as $operator) {
$stream = $operator($stream);
}
return $stream;
}
/
- ストリームを購読(Subscribe)し、イベントの消費を開始する
/
public function subscribe(callable $onNext, ?callable $onError = null, ?callable $onComplete = null): void
{
// 各購読ごとに独立したFiberを生成し、Zend VMのスタック空間を隔離する
$fiber = new Fiber(function () {
($this->producer)(function ($value) {
// 値が流れてきたら、親のFiber(イベントループ側)へ一時停止して値を渡す
Fiber::suspend($value);
});
});
try {
// 初回実行
$value = $fiber->start();
// イベントループ的な駆動:Fiberが終了するまで値を消費し続ける
while (!$fiber->isTerminated()) {
if ($value !== null) {
$onNext($value);
}
// 次の値を要求してFiberを再開
$value = $fiber->resume();
}
if ($onComplete) {
$onComplete();
}
} catch (Throwable $e) {
if ($onError) {
$onError($e);
} else {
throw $e;
}
}
}
}
このコードの美しいところは、`$producer` の内部でどれだけ複雑な非同期I/O待ちやタイマー処理があっても、呼び出し側(Consumer)からは「まるで同期処理のように、上から下に流れるストリーム」に見える点です。Fiberがコールスタックを丸ごと保持しているため、例外が発生してもスタックトレースが綺麗に保たれます。
—
3. オペレーター(map, filter)の実装:関数型パターンの統合
リアクティブプログラミングの真骨頂は、`map` や `filter` といった高階関数オペレーターをチェイン(パイプ)できることにあります。先ほどの `Observable` に流れるデータを加工するオペレーターを実装してみましょう。
namespace TinyRx;
class Operators
{
/
- 流れてくるデータを変換する map オペレーター
/
public static function map(callable $fn): callable
{
return function (Observable $source) use ($fn) {
return new Observable(function ($observer) use ($source, $fn) {
$source->subscribe(
onNext: fn($value) => $observer($fn($value)),
onError: fn($e) => throw $e,
onComplete: fn() => null
);
});
};
}
/
- 条件に合致するデータのみを通す filter オペレーター
/
public static function filter(callable $predicate): callable
{
return function (Observable $source) use ($predicate) {
return new Observable(function ($observer) use ($source, $predicate) {
$source->subscribe(
onNext: function($value) use ($observer, $predicate) {
// 条件に一致する場合のみ、下流へプッシュする
if ($predicate($value)) {
$observer($value);
}
},
onError: fn($e) => throw $e,
onComplete: fn() => null
);
});
};
}
}
—
4. 実践:構築したフレームワークを動かしてみる
では、私たちが今作ったばかりのFiberベース・リアクティブフレームワークを実際に動かしてみましょう。
ここでは、擬似的な非同期イベントソース(例えば、WebSocketのメッセージ受信やDBからのストリーミングデータ)を想定します。
pipe(
// 偶数だけを抽出するフィルター
Operators::filter(fn($val) => $val % 2 === 0),
// 抽出された値を2倍に変換するマップ
Operators::map(fn($val) => $val 10)
)->subscribe(
onNext: function ($result) {
echo “受信データ: {$result}\n”;
},
onError: fn($e) => echo “エラー発生: ” . $e->getMessage() . “\n”,
onComplete: fn() => echo “すべての処理が完了しました。\n”
);
実行結果
— ストリーム処理開始 —
受信データ: 20
受信データ: 40
— ストリーム処理終了 —
すべての処理が完了しました。
奇数(1, 3, 5)は見事にフィルターされ、偶数(2, 4)のみが `10` 倍されて綺麗にコンソールに出力されましたよね。しかも、この一連の処理は複雑なコールバックの入れ子ではなく、Fiberのコンテキストスイッチによって美しく制御されています。
—
5. アーキテクトからのメッセージ:裏側のメモリ管理と注意点
最後に、PHPでFiberを扱う上での重要なアーキテクチャ上の注意点をお伝えします。
Zend VMにおいて、通常のリクエストは単一のコールスタック(CスタックおよびZendの実行スタック)上で直線的に処理されます。しかし、`new Fiber()` を生成すると、Zendエンジンはヒープメモリ上に独立した実行コンテキスト(スタックフレームの集合)を割り当てます。
つまり、Fiberを無制限に生成・放置すると、PHPのメモリ消費量が跳ね上がる原因になります。Node.jsの軽量スレッド感覚で数万個のFiberを同時に立ち上げるような設計は、現在のPHP(Zend VM)のメモリ管理機構においては避けるべきです。
あくまで「I/Oバウンドな特定の非同期処理のブロックを避けるため」「複雑な非同期ストリームを読みやすくするため」の特効薬として、適切な粒度で使い分けることが、プロフェッショナルなPHPアーキテクトの腕の見せ所となります。
PHPの裏側で何が起きているのかをイメージできるようになると、コードを書く手が止まらなくなるはずです。ぜひ、今日の知見をご自身のプロダクトの設計やフレームワークの拡張に応用してみてください。
あなたのPHPライフが、もっとエキサイティングで美しいものになりますように。