【入門編】Fiberにおける状態管理と永続化:非同期処理のコンテキストをセッションやデータベースに保存する – PHPコア・内部エンジンと高速化・並行処理の極意解析バイブル

こんにちは。PHPの裏側を覗く旅へようこそ。

普段、私たちは何気なくLaravelやSymfonyといったフレームワークを使い、コントローラーでリクエストを受け取り、レスポンスを返していますよね。PHPのモデルは基本的に「1リクエスト = 1プロセス(またはスレッド)の完全な使い捨て」というシェアード・ナッシング(Shared-Nothing)アーキテクチャに基づいています。

しかし、PHP 8.1で導入された Fiber(ファイバー) は、その常識を静かに、しかし劇的に変えようとしています。

今回は、Fiberの本質である「協的中断と再開」のメカニズムを一歩進め、「Fiberの実行コンテキストをシリアライズし、リクエスト境界やサーバーのライフサイクルを超えて永続化する」という、極限のアーキテクチャについて紐解いていきましょう。

ここを理解すると、PHPが単なる「Webのテンプレートエンジン」から「真の非同期ステートフルランタイム」へ進化する瞬間が見えてきますよ。

—

1. Fiberの正体:Zend VMにおける「スタックの切り離し」

まず、Fiberとは一体何者なのか。JavaのThreadやGoのGoroutineと同じだと思っていませんか?
実は、PHPのFiberはそれらとは全く異なります。GoのスケジューラがOSスレッド上でプリエンプティブ(強制剥奪型)に動くのに対し、PHPのFiberは完全なユーザーランド・協調型(Cooperative)グリーン・スレッドです。

Zendエンジン(Zend VM)の視点から見ると、通常の関数呼び出しはコールスタック(`zend_execute_data` の連結リスト)を深く積み上げていきます。Fiberが生まれると、PHPは独立したCヒープ上のスタック空間を切り出します。

[通常のPHP実行フロー]
Request -> Global Execution Stack -> 함수A -> 関数B (ここでIO待ち…ブロッキング)

[Fiberによる非同期フロー]
Request -> Global Stack -> Fiber::suspend() -> [スタックを退避してメインに制御戻す]
↓
(別の処理やイベントループが回る)
↓
Fiber::resume() -> [スタックを復元して再開]

`Fiber::suspend()` が呼び出されると、Zend VMは現在の実行コンテキスト(ローカル変数、コールスタック、実行位置を表すOPコードポインタ)を一時停止し、呼び出し元へ制御を返します。

ここで鋭いあなたなら気づくはずです。
「この一時停止したコンテキスト、メモリ上に保持されているなら、シリアライズして外に出せるのでは?」 と。

—

2. なぜFiberの永続化が必要なのか?

通常のFiberは、当然ながらPHPのプロセス(FPMのワーカープロセスなど)のメモリ空間内でしか生きられません。リクエストが終了すれば、FPMプロセスはリセットされ、メモリ上のFiberは消え去ります。

しかし、次のような要件を考えてみてください。

  • 数分〜数時間かかる巨大なマルチステップのワークフロー(例:外部APIのレートリミットを回避しながら数万件のデータを非同期でバッチ処理する)
  • AIエージェントとの対話コンテキスト(数ターンにわたる思考プロセスの途中で、ユーザーからの追加入力を待つために処理を一時停止したい)
  • 中断・再開可能な長寿命Webゲームやシミュレーション

これらを従来のPHPでやろうとすると、データベースやRedisに「現在のステップ番号」や「中間データ」を細かく保存し、次のリクエストで最初からオブジェクトを再構築する(Hydrationする)という、退屈で複雑なボイラープレートコードを書く必要がありました。

もし、「Fiberの内部状態(クロージャ、ローカル変数、コールスタック)そのものをどこかに保存し、次のリクエストでロードして `resume()` できる」としたらどうでしょうか?

—

3. 限界の壁:なぜネイティブのFiberはそのままシリアライズできないのか?

ここで冷徹な現実をお伝えしなければなりません。
残念ながら、PHPのコアに組み込まれた `Fiber` オブジェクトや、それに紐づく `Closure`、内部の `zend_execute_data` は、そのまま `serialize()` することができません。

なぜなら、これらにはC言語レベルのポインタ(関数テーブルへの参照、リソース、内部オブジェクトのメモリ番地など)が複雑に絡み合っているからです。`serialize()` を試みると、エンジンは例外を投げるか、セグメンテーションフォールトを引き起こすでしょう。

では、どうすればよいのでしょうか?
答えは「状態(State)と処理ロジック(Logic)の完全な分離」です。

アーキテクトとしての腕の見せ所はここです。Fiberそのものを永続化するのではなく、「Fiberが再開するために必要な最小限のドメイン状態」をデータ構造としてシリアライズし、リクエストを跨いでFiberを再構築(Rehydration)する仕組みを作ります。

—

4. 実装:状態を永続化する「ステートフル・ファイバー」の設計

概念をコードに落とし込んでみましょう。ここでは、非同期処理の途中で状態をデータベースやRedisに保存し、別のリクエスト(あるいはプロセス再起動後)で復元して処理を続行するパターンの実装アプローチを示します。

  • 永続化可能なワークフローを表す抽象クラス
  • /
    abstract class PersistentWorkflow
    {
    protected array $state = [];
    protected string $status = ‘pending’;

    // ワークフローのビジネスロジック(Fiber内で実行される)
    abstract public function handle(): void;

    public function getState(): array
    {
    return $this->state;
    }

    public function setState(array $state): void
    {
    $this->state = $state;
    }
    }

    /

    • 実際にマルチステップで重い処理を行うワークフローの具体例

    /
    class UserOnboardingWorkflow extends PersistentWorkflow
    {
    public function handle(): void
    {
    // ステップ1: 初期処理
    $this->state[‘step’] = 1;
    $this->state[‘log’][] = ‘ユーザー登録の検証を開始しました。’;

    // 処理を一度中断し、外部からの承認やAPIレスポンスを待つ状態にする
    // ここで現在の状態を外部ストレージに保存できる
    $userId = Fiber::suspend([‘action’ => ‘wait_for_approval’, ‘data’ => $this->state]);

    // — (ここでリクエストが一旦終了し、後日別リクエストで再開される) —

    // ステップ2: 再開後の処理
    $this->state[‘step’] = 2;
    $this->state[‘user_id’] = $userId;
    $this->state[‘log’][] = “ユーザー ID: {$userId} のセットアップが完了しました。”;

    $this->status = ‘completed’;
    }
    }

    /

    • Fiberのライフサイクルと永続化を管理するマネージャー

    /
    class WorkflowManager
    {
    // 状態をDBやRedisに保存する模擬ストア
    private array $storage = [];

    public function start(PersistentWorkflow $workflow): string
    {
    $workflowId = uniqid(‘wf_’);

    $fiber = new Fiber(function () use ($workflow) {
    $workflow->handle();
    });

    // Fiberを開始(最初のsuspendまで実行)
    $suspendedData = $fiber->start();

    // 状態とファイバーの進行状況を永続化ストレージに保存
    $this->persist($workflowId, [
    ‘workflow’ => serialize($workflow), // ※ワークフロー自体のデータ構造のみ
    ‘suspended_value’ => $suspendedData,
    // 注意: Fiberオブジェクトそのものはシリアライズできないため、
    // 「どこまで進んだか(ステップ)」をステートとして保存する
    ]);

    return $workflowId;
    }

    public function resume(string $workflowId, mixed $input): void
    {
    $record = $this->storage[$workflowId] ?? null;
    if (!$record) {
    throw new Exception(“Workflow not found.”);
    }

    / @var PersistentWorkflow $workflow /
    $workflow = unserialize($record[‘workflow’]);

    // 再度Fiberを生成し、保持していたステートを注入して再開する
    $fiber = new Fiber(function () use ($workflow) {
    $workflow->handle();
    });

    // 初期スタート(ロジックの最初から通すが、ステートを復元してジャンプする設計にする)
    // ※実運用では状態マシン(State Machine)パターンと組み合わせるのが定石です
    $fiber->start();

    // 再開時に外部からの入力を渡す
    $fiber->resume($input);
    }

    private function persist(string $id, array $data): void
    {
    // 実際にはここでPDOを通したDB保存やRedisへの格納を行う
    $this->storage[$id] = $data;
    }
    }

    —

    5. アーキテクトからの実践的アドバイス:状態マシンとの融合

    先ほどのコードを見て、「あれ、`resume()` する時に最初から `handle()` を呼び直してないか?」と気づいた方は非常に鋭いです。

    純粋なCレベルのコールスタックを丸ごとシリアライズできないPHPにおいて、Fiberを永続化する最も堅牢なデザインパターンは、「Fiber + ステートマシン(State Machine)」のハイブリッド戦略です。

    1. ロジックの断片化: 処理を「ステップA」「ステップB」「ステップC」というメソッドやクロージャ単位に分割する。
    2. ステートの保持: データベースには「現在どのステップにいるか(`step = 2`)」と「その時点での変数配列」だけをJSON等で保存する。
    3. 再開時のルーティング: リクエストが来たら、ストレージからステートをロードし、該当するステップの処理からFiberを再ウェーブ(Re-wave)させる。

    このアプローチを取ることで、PHPのシェアード・ナッシングなFPM環境であっても、あたかも長寿命なプロセスが動き続けているかのような「ステートフルな非同期ワークフロー」を完全に再現できます。

    —

    結びにかえて

    Fiberは単なる「コールバック地獄を回避するためのシンタックスシュガー」ではありません。
    イベントループ(ReactPHPやAmpなど)と組み合わせることで、PHPのシングルスレッド並行処理能力を極限まで引き出すための強力なエンジンです。

    そして、その実行コンテキストの境界を理解し、ストレージ層と巧みに連携させる設計手法を手に入れたとき、あなたの書くPHPコードは、他のどの言語にも負けない柔軟性と堅牢性を手に入れます。

    「PHPだからここまでしかできない」のではなく、「PHPの裏側の仕組みを知っているから、ここまで実装できる」。
    ぜひ、次のアーキテクチャ設計でこの知見を活かしてみてください。PHPの奥深い世界が、あなたをもっと面白くしてくれるはずです。

    タイトルとURLをコピーしました