PHP 8.1以降におけるFiberの深層:バージョン間互換性と、実務システムへの非同期並行処理導入の極意
テックリードの私たちがコードレビューで最も恐れるのは、「動くが、内部構造を破壊するコード」だ。PHP 8.1で導入された `Fiber` は、長年PHPの足枷とされてきた「共有何もなし・リクエスト完結型の同期的実行モデル」に風穴を開けた。コールバック地獄(Pyramid of Doom)を生むことなく、協調的マルチタスク(Cooperative Multitasking)を実現する画期的なプリミティブである。
しかし、これを「なんとなく非同期っぽく動く便利な構文」として既存のモノリスやレガシーコードに安易に導入すれば、Zend VMのコールスタックの破壊、メモリリーク、そしてサードパーティライブラリの予期せぬブロッキングという致命傷を負う。
本稿では、FiberがPHP内部(Zend VM)でどのように扱われているかという低レイヤの現実を踏まえ、バージョンごとの挙動の違い、そして既存システムへ安全にFiberを組み込むための堅牢なアップグレードパスと設計ルールを伝授する。
—
1. Zend VMの視点から見るFiber:内部で何が起きているのか
従来のPHP(PHP 8.0以前)では、1つのリクエストにつき1つのCスタック(正確にはZendの実行コンテキスト)が線形に消費されていた。関数が呼び出されるたびに `zend_execute_data` がスタック上に積み上がり、returnで巻き戻される。
`Fiber` が導入されたことにより、PHPはユーザーランドから制御可能な複数の実行コンテキスト(スタックフレームの集合)を持てるようになった。
[ PHP Request ]
└─ FPM Process
├─ Main Fiber (Zend Execution Context A)
└─ User Fiber (Zend Execution Context B – 独自のスタックを持つ)
├─ Fiber::suspend() ──> 控制をメインへ返却 (スタックの状態を保持)
└─ Fiber::resume() ──> 状態を復元して再開
バージョン間の差異と落とし穴
- PHP 8.1: Fiberの基本実装。サスペンド・レジュメの基本機能が提供されたが、Cレベルの拡張機能(ext-mysqlなど)をまたぐサスペンドは依然として不可能(ブロッキングI/OはFiberの実行スレッド全体を止める)。
- PHP 8.2 – 8.3: エラーハンドリングの洗練と、メモリ管理の最適化。特に `Fiber` 内で例外がスローされた場合の巻き戻し処理の堅牢性が向上。
- PHP 8.4以降を見据えた注意点: 非同期I/Oをネイティブでサポートするイベントループ(Amp v3やReactPHPのFiber統合版など)と組み合わせる際、`ext-sockets` や `stream_select` の挙動に依存するため、PHPのマイナーバージョンアップごとのストレージ・ネットワークドライバの挙動差異に細心の注意が必要となる。
—
2. 既存システムへのFiber導入:4つの鉄則
もし君のプロジェクトが、LaravelやSymfonyをベースとした、あるいはレガシーな素のPHPによるWebアプリケーションであれば、以下のルールを破った瞬間にシステムは崩壊する。
鉄則1: 「ブロッキングI/O」をFiberで包んでも速くならない
Fiberはプリエンプティブ(強制割り込み)ではなく、協調的(コーペラティブ)である。つまり、コード自身が `Fiber::suspend()` を呼ばない限り、CPUやI/Oを明け渡さない。
データベースへの同期的なクエリ(`PDO` によるクエリ発行など)の最中にFiberをサスペンドさせることはできない。非同期化の恩恵を受けるには、I/O待機をイベントループに委譲するライブラリ(AmpやReactPHPのFiberアダプター)と組み合わせる必要がある。
鉄則2: グローバルステートとシングルトンの呪縛
PHPの多くのフレームワークは、リクエストスコープを前提としたシングルトン(例: データベース接続インスタンス、リクエストコンテキスト)で満たされている。
同一プロセス(同一FPMワーカー)内で複数のFiberを並行稼働させた場合、グローバル変数やstaticプロパティがFiber間で共有され、データ競合(Data Race)やコンテキストの混濁が発生する。
鉄則3: 例外の伝播ロスを見逃すな
Fiber内部で未キャッチの例外が発生した場合、そのFiberをresumeした瞬間に例外が投げ返される。これを適切にハンドリングしないと、イベントループ全体のクラッシュを招く。
—
3. 実務で使える:安全なFiber駆動型非同期クライアントの実装例
以下に、外部APIへの並行リクエストをFiberと自前でミニマルに制御するイベントループ風スケジューラを組み合わせた、実務に耐えうる堅牢なリファレンスコードを示す。
このコードは、バージョン差異による挙動のブレを防ぎつつ、メモリ効率と例外安全性を担保した設計になっている。
/
class FiberScheduler
{
/ @var \SplQueue
private \SplQueue $queue;
private int $activeCount = 0;
public function __construct()
{
$this->queue = new \SplQueue();
}
/
- タスク(Fiber)をスケジューラに登録する
/
public function add(callable $task, mixed …$args): void
{
$fiber = new Fiber(function () use ($task, $args) {
try {
// タスクを実行し、結果を返す
return $task(…$args);
} catch (\Throwable $e) {
// Fiber内部での例外を捕捉し、ログ記録または安全に伝播させる
error_log(sprintf(“[Fiber Error] %s in %s:%d”, $e->getMessage(), $e->getFile(), $e->getLine()));
throw $e;
}
});
$this->queue->enqueue([$fiber, null]);
$this->activeCount++;
}
/
- スケジューラの実行を開始し、すべてのFiberが完了するまでブロックする
- @return array
各Fiberの実行結果
/
public function run(): array
{
$results = [];
while (!$this->queue->isEmpty()) {
[$fiber, $value] = $this->queue->dequeue();
try {
if (!$fiber->isStarted()) {
// 初回実行
$output = $fiber->start();
} else {
// サスペンド状態からの復帰
$output = $fiber->resume($value);
}
if ($fiber->isTerminated()) {
// Fiberの正常終了
$results[] = $fiber->getReturn();
$this->activeCount–;
} else {
// まだ処理が残っている(サスペンドされた)場合、キューの末尾に戻す
// ※実際の非同期I/Oでは、ここでイベントループの監視登録を行う
$this->queue->enqueue([$fiber, $output]);
}
} catch (\Throwable $e) {
// 異常終了時の処理
$this->activeCount–;
// 必要に応じて例外を上位にスロー
throw $e;
}
}
return $results;
}
}
// ==========================================
// 実行・検証用スクリプト(実務での利用シーン)
// ==========================================
// ダミーの非同期APIフェッチを模したタスク
$mockApiCall = function (string $endpoint, int $delayMs) {
echo “> Request start: {$endpoint}\n”;
// PHPの sleep() はブロッキングするため、Fiber内では厳禁。
// 代わりに模擬的なサスペンドを発生させる(実環境ではsocketのnon-blocking待機)
$start = microtime(true);
while ((microtime(true) – $start) 1000 < $delayMs) {
// CPUを一時的に明け渡す(協調的マルチタスクのシミュレーション)
Fiber::suspend("waiting_for_{$endpoint}");
}
echo "< Request finished: {$endpoint}\n";
return "Data from {$endpoint}";
};
$scheduler = new FiberScheduler();
// 3つの異なるタスクを並行(疑似的)に登録
$scheduler->add($mockApiCall, ‘https://api.example.com/v1/users’, 100);
$scheduler->add($mockApiCall, ‘https://api.example.com/v1/orders’, 50);
$scheduler->add($mockApiCall, ‘https://api.example.com/v1/products’, 150);
$executionStart = microtime(true);
try {
$results = $scheduler->run();
print_r($results);
} catch (\Throwable $e) {
echo “Fatal error during fiber execution: ” . $e->getMessage() . “\n”;
}
$executionTime = microtime(true) – $executionStart;
echo sprintf(“All tasks completed in %.4f seconds.\n”, $executionTime);
—
4. コードレビューで指摘すべき「危険なアンチパターン」
チームメンバーがFiberを導入したコードを持ってきた際、以下のコードが含まれていたら即座に差し戻すべきだ。
アンチパターンA: Fiber内でのブロッキングI/O(PDO / File操作)の放置
// ❌ 危険:データベースクエリはブロッキングするため、
// Fiberを使っても実質的に全体の処理速度は直列実行と変わらず、メモリスタックの無駄遣いになる。
$fiber = new Fiber(function() {
$pdo = new PDO(‘mysql:host=localhost;dbname=test’, ‘user’, ‘pass’);
$stmt = $pdo->query(‘SELECT FROM heavy_table’); // ここでOSスレッドが完全にブロックされる
return $stmt->fetchAll();
});
修正指針: データベースアクセスを非同期化したい場合は、`Amp\Mysql` や `ReactPHP` の非同期ドライバ、またはWorkers(pcntlによるプロセス分離など)の検討を行え。Fiber単体はCPUバウンドな処理のコンテキストスイッチや、非同期I/Oイベントループの橋渡しのためにある。
アンチパターンB: メモリリークを招く循環参照
Fiberはクロージャを保持して生成されるため、クロージャ内で外部の大きなオブジェクト(ORMのエンティティマネージャなど)を `$this` やuse構文でキャプチャし続けると、Fiberが終了(terminate)してガベージコレクションに回収されるまでの間、メモリ空間に巨大なオブジェクトが居座り続ける。
// ❌ 危険:巨大なサービスコンテナやORMマネージャをキャプチャしている
$bigObject = new MassiveServiceContainer();
$fiber = new Fiber(function() use ($bigObject) {
// 処理…
});
—
結び:アーキテクトとしての判断
FiberはPHPの表現力を一段上のレイヤーへと引き上げた。しかし、「PHPはスクリプト言語だからメモリ管理はエンジン任せでいい」という甘えは、Fiberを用いた並行処理の前では通用しない。
バージョンごとの挙動の差異を把握し、Zend VMのスタック構造とメモリライフサイクルを脳内でトレースできる者だけが、高速で堅牢な非同期PHPアプリケーションを構築する資格を持つ。
既存システムへの導入は、まずは限られたI/Oバウンドなマイクロサービス的コンポーネント(Webhookのバッチ送信や外部APIの並行フェッチなど)の限定的なスコープからスモールスタートせよ。それが、システムを止めずに近代化を果たす唯一にして最良のパスである。