【実務・中級編】Fiberとアクターモデルの比較:並行処理モデルの選択基準とエンタープライズ適用 – PHPコア・内部エンジンと高速化・並行処理の極意解析バイブル

PHPを掌握する極限の知見:Fiberとアクターモデルの交差点 – エンタープライズ並行処理の選択基準

PHP 8.1における `Fiber`(ファイバー)の導入は、長年「共有何もなし(Shared-Nothing)」の生命線としてリクエストライフサイクルを完結させてきたPHPのランタイムに、コルーチンベースの協調的マルチタスキング(Cooperative Multitasking)というパラダイムをもたらした。

しかし、アーキテクトとして冷静に問い直さねばならない。
「お前は本当にそのFiberを使いこなせているか? 非同期という言葉に酔い、メモリリークとステートの汚染を招いていないか?」

今回は、スケーラビリティの文脈で語られることの多い「アクターモデル(Actor Model / Akka等)」の設計思想をPHPのFiberと比較し、エンタープライズシステムにおいてどちらを選択すべきか、Zend VMのメモリ管理とイベントループの挙動を踏まえて徹底的に解剖する。

—

1. 根本思想の比較:Fiber(協調的) vs アクターモデル(隔離とメッセージング)

並行処理モデルを選ぶ際、我々は「データの共有と制御」のコストをどこに払うかを決定しなければならない。

Fiberの正体:スタックフル・コルーチンとZend VMの裏側

PHPの `Fiber` は、ユーザーランドで実行コンテキスト(コールスタック、ローカル変数、実行ポインタ)をオブジェクトとしてカプセル化する。
C10K問題を解決するための非同期I/O(ReReactPHPやAmp v3等との組み合わせ)において、コールバック地獄を回避する強力な武器となる。

しかし、忘れてはならないのは、Fiberはプリミティブな実行制御の単位に過ぎないという点だ。

  • 共有メモリの罠: スレッドとは異なり、同一プロセス内のヒープメモリ空間やグローバルステート(あるいはシングルトン、静的プロパティ)はすべてのFiber間で完全に共有される。
  • 競合状態(Race Condition)の発生: あるFiberが `yield` し、別のFiberに制御が移っている間に、共有ステートが書き換えられる危険性がある。PHPはシングルスレッドで動いているとはいえ、「I/O待ちによる中断」のタイミングでコンテキストが切り替わるため、非同期的なバグの温床となる。

アクターモデルの正体:完全なカプセル化とメールボックス

一方、ScalaのAkkaやErlang/Elixirに代表されるアクターモデルは、ステートの共有を完全に拒絶する。

  • 共有なし(Shared-Nothing)の徹底: 各アクターは自身のプライベートな状態を持ち、他のアクターの内部を直接覗くことはできない。
  • メッセージパッシング: 状態を変更するには、アクターの「メールボックス」にメッセージを非同期で送るしかない。メールボックスからの取り出しは直列化(Sequential)されるため、ロックフリーかつスレッドセーフ(PHPの文脈ではプロセス/イベントループセーフ)に状態を管理できる。

—

2. エンタープライズ適用における選択基準

では、数百万アクセスのAPIや、複雑なドメインロジックを持つエンタープライズPHPアプリケーションにおいて、どちらを選ぶべきか。結論から言えば、「I/Oバウンドな高スループット処理にはFiber」「複雑なステート管理とフォールトトレランス(耐障害性)が求められるドメインにはアクターモデル的設計」となる。

以下のマトリクスをコードレビューの基準として記憶してほしい。

| 評価軸 | Fiberベースの非同期処理 | アクターモデル的設計(PHPでのエミュレーション) |
| :— | :— | :— |
| 主な用途 | HTTPクライアントの並列実行、DB/Redisの非同期バルク取得 | リアルタイムチャット、分散ワークフロー、ステートフルなセッション管理 |
| メモリ効率 | 高い(軽量だが、Zend VMのスタック領域を消費) | 中程度(アクターごとのオブジェクトオーバーヘッド) |
| デバッグ難易度 | 高い(スタックトレースが断片的になる) | 低い(メッセージ単位でトレーサビリティを確保しやすい) |
| 耐障害性 | 局所的な例外がイベントループ全体を落とすリスクあり | 「Let it crash(エラーたらしめよ)」哲学による高い復元力 |

—

3. 実装:Fiberを用いた安全な並行APIクライアントと限界

まずは、実務で最も恩恵を受けやすい、Fiberとイベントループを組み合わせた並行HTTPリクエスト処理の実装を見てみよう。
単にFiberを乱立させるのではなく、「例外の伝播」と「メモリリーク防止」を考慮したプロダクション品質のコードだ。

  • 簡易的なイベントループとFiberを統合した非同期タスクランナー。
  • 実務では ReactPHP や Amp のコンポーネントを利用すべきだが、
  • 内部で何が起きているかを把握するためプリミティブな実装を示す。
  • /
    class AsyncDispatcher
    {
    / @var array /
    private array $fibers = [];
    private array $results = [];
    private array $errors = [];

    /

    • タスク(Fiber化された処理)を追加する

    /
    public function add(string $key, callable $task): void
    {
    $fiber = new Fiber(function () use ($task) {
    // タスクを実行し、結果を返す
    return $task();
    });

    $this->fibers[$key] = $fiber;
    }

    /

    • すべてのFiberを協調的に実行する

    /
    public function run(): void
    {
    // 起動フェーズ
    foreach ($this->fibers as $key => $fiber) {
    try {
    $fiber->start();
    } catch (Throwable $e) {
    $this->errors[$key] = $e;
    }
    }

    // イベントループのモック:Fiberがサスペンド(yield)している間のハンドリング
    // ※実プロダクトではここでソケットのreadable/writableを監視する
    while ($this->hasActiveFibers()) {
    foreach ($this->fibers as $key => $fiber) {
    if ($fiber->isSuspended()) {
    try {
    // 再開(Resume)。実際の非同期I/Oでは、データが到着したタイミングで呼ぶ
    $fiber->resume();
    } catch (Throwable $e) {
    $this->errors[$key] = $e;
    }
    }

    if ($fiber->isTerminated()) {
    if (!isset($this->errors[$key])) {
    $this->results[$key] = $fiber->getReturn();
    }
    unset($this->fibers[$key]);
    }
    }

    // CPUのスパイクを防ぐための極小スリープ(イベントループのTickに相当)
    usleep(1000);
    }
    }

    private function hasActiveFibers(): bool
    {
    foreach ($this->fibers as $fiber) {
    if (!$fiber->isTerminated()) {
    return true;
    }
    }
    return false;
    }

    /

    • @return array

    /
    public function getResults(): array
    {
    return $this->results;
    }

    /

    • @return array

    /
    public function getErrors(): array
    {
    return $this->errors;
    }
    }

    // ==========================================
    // 使用例:複数の外部APIを並行して叩くシナリオ
    // ==========================================
    /
    $dispatcher = new AsyncDispatcher();

    $dispatcher->add(‘user_profile’, function () {
    // 内部でI/O待ちが発生したと仮定してFiberを一時停止(Suspended)させるシミュレーション
    // 実コードでは curl_multi や Amp\http-client を使用
    Fiber::suspend();
    return [‘id’ => 1, ‘name’ => ‘Architect’];
    });

    $dispatcher->add(‘user_orders’, function () {
    Fiber::suspend();
    return [[‘order_id’ => 101], [‘order_id’ => 102]];
    });

    $dispatcher->run();

    print_r($dispatcher->getResults());
    /

    このコードのアーキテクチャ的解説

    Zend VMの観点から、上記のコードは「スタックの巻き戻しと再開」をユーザーランドで行っている。
    `Fiber::suspend()` が呼ばれた瞬間、PHPの実行コンテキストは一度親スコープ(イベントループ)に制御を返し、メモリ上のコールスタックの状態が退避される。
    これにより、ブロッキングI/OによるCPUの遊休時間を劇的に削減できる。

    —

    4. 応用:PHPにおける「アクターモデル」的エンタープライズ設計

    では、複雑な業務ドメイン(例:ECの在庫引当と決済の整合性担保など)において、Fiberの「共有ステートの危険性」を排除し、アクターモデルの思想をPHP(RoadRunnerやFrankenPHPなどの長期稼働プロセス環境)で実装するにはどうすればよいか。

    以下のコードは、「メールボックスを持つ独立したステート管理クラス(アクター)」の概念をPHPで表現したリファレンスである。

  • アクターの基底抽象クラス。
  • 状態(State)をカプセル化し、メッセージ(Message)の処理を直列化する。
  • /
    abstract class AbstractActor
    {
    / @var array メールのキュー(メールボックス) /
    private array $mailbox = [];

    private bool $isProcessing = false;

    /

    • メッセージをアクターのメールボックスに送信する(スレッド/リクエストセーフ)

    /
    public function tell(mixed $message): void
    {
    $this->mailbox[] = $message;
    $this->drainMailbox();
    }

    /

    • メールボックスを順次処理する(直列化の保証)

    /
    private function drainMailbox(): void
    {
    // 再入可能性(Reentrancy)を防ぐガード
    if ($this->isProcessing) {
    return;
    }

    $this->isProcessing = true;

    try {
    while (!empty($this->mailbox)) {
    $message = array_shift($this->mailbox);
    // 各具象アクターで定義されたビジネスロジックを実行
    $this->receive($message);
    }
    } finally {
    $this->isProcessing = false;
    }
    }

    /

    • 具象アクターで実装するメッセージハンドラ

    /
    abstract protected function receive(mixed $message): void;
    }

    /

    • 具体的なアクター:銀行口座の残高を安全に管理する
    • (競合状態を完全に排除したステートフルオブジェクト)

    /
    class BankAccountActor extends AbstractActor
    {
    private int $balance;

    public function __construct(int $initialBalance)
    {
    $this->balance = $initialBalance;
    }

    protected function receive(mixed $message): void
    {
    if (!is_array($message) || !isset($message[‘type’])) {
    return;
    }

    switch ($message[‘type’]) {
    function_exists(‘log_message’) ?: null; // ダミー

    case ‘DEPOSIT’:
    $amount = $message[‘amount’];
    $this->balance += $amount;
    // 実際にはここでロガーや永続化層へイベントを発行する
    echo “[BankAccount] Deposited: {$amount}. New Balance: {$this->balance}\n”;
    break;

    case ‘WITHDRAW’:
    $amount = $message[‘amount’];
    if ($this->balance < $amount) { echo "[BankAccount] Error: Insufficient funds for withdrawal of {$amount}\n"; break; } $this->balance -= $amount;
    echo “[BankAccount] Withdrawn: {$amount}. New Balance: {$this->balance}\n”;
    break;

    case ‘GET_BALANCE’:
    // 読み取り専用メッセージに対する応答(コールバック等で返す設計も可能)
    echo “[BankAccount] Current Balance: {$this->balance}\n”;
    break;
    }
    }
    }

    // ==========================================
    // 実行シミュレーション
    // ==========================================
    /
    $account = new BankAccountActor(1000);

    // 非同期(あるいは並行)にメッセージをガンガン投げ込んでも、
    // 内部の `drainMailbox` と `isProcessing` ガードにより、処理は必ず直列化され、
    // 競合状態(Race Condition)は絶対に発生しない。
    $account->tell([‘type’ => ‘DEPOSIT’, ‘amount’ => 500]);
    $account->tell([‘type’ => ‘WITHDRAW’, ‘amount’ => 200]);
    $account->tell([‘type’ => ‘GET_BALANCE’]);

    // 出力結果:
    // [BankAccount] Deposited: 500. New Balance: 1500
    // [BankAccount] Withdrawn: 200. New Balance: 1300
    // [BankAccount] Current Balance: 1300
    /

    アーキテクトからの厳しい警告

    PHPの長期稼働プロセス(RoadRunner, Swoole, FrankenPHP)において、グローバル変数やシングルトンにドメインのステートを持たせることは、システム全体を崩壊させる「時限爆弾」を仕掛けるに等しい。
    Fiberを使う場合であっても、共有変数へのアクセスを慎重に制御しない限り、マルチスレッドプログラミングにおける悪名高いデッドロックやダーティリードのPHP版に直面することになる。

    エンタープライズシステムにおいて、複雑なステートやトランザクションの整合性がビジネスの生命線であるならば、Fiberの生な制御に頼るのではなく、アクターモデル的な「状態の局所化とメッセージングによる疎結合化」を設計思想の根底に据えるべきだ。

    —

    5. まとめ:モダンPHPアーキテクチャの選択指針

    1. 外部APIコール、DBのバルク並列取得、ファイルI/Oの高速化
    $\rightarrow$ Fiber を採用せよ。ただし、例外処理のスコープとメモリリーク(循環参照や長命Fiberへのクロージャ束縛)には細心の注意を払うこと。
    2. 複雑なドメインロジック、セッション状態の維持、分散ワークフローの制御
    $\rightarrow$ アクターモデル的設計(ステートのカプセル化とメッセージパッシング)を採用せよ。Zend VMのヒープを汚染しない、堅牢でクリーンなアーキテクチャ構築が可能となる。

    「ただ動くコード」を書く時代は終わった。
    我々テクニカルリードは、ランタイムのメモリ構造と実行フローの裏側を完全に支配し、ビジネスのスケールに耐えうる美しいシステムをコードで証明し続けなければならない。

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