【実務・中級編】Swoole CoroutineとPHPネイティブFiberのパフォーマンス比較と使い分け – PHPコア・内部エンジンと高速化・並行処理の極意解析バイブル

Swoole CoroutineとPHPネイティブFiberの徹底比較:Zend VMのスタック破壊を防ぐ非同期プログラミングの極意

テックリードの私だ。コードレビューで「とりあえずコルーチンにしておけば速くなるんでしょ?」という甘い認識のプルリクエストを見かけるたびに、私の頭痛の種が増える。

PHP 8.1でFiber(ファイバー)がネイティブに実装され、SwooleやReactPHPといったサードパーティ製拡張モジュールに頼らずとも、PHP単体で協的中断・再開(Cooperative Multitasking)が可能になった。これにより、PHPの非同期プログラミングの歴史は大きな転換点を迎えた。

だが、エンジニアの勘違いもここで加速した。「ネイティブのFiberが入ったなら、もうSwooleはいらないのでは?」「いや、Swooleのコルーチンの方が高スループットだ」――この手の議論に終止符を打つため、今回はZend VMの低レイヤの挙動からメモリ管理、I/O多重化の仕組みまでを解剖し、実務でどちらを選ぶべきかの基準を明確に提示する。

—

1. 根本的な思想の違い:Zend VMスタックとI/Oドライバ

まず大前提として、SwooleのCoroutineとPHPネイティブのFiberは、解決しようとしている問題のレイヤが異なる。

PHP Fiberの正体:ユーザーランド・スタック

Fiberは、Zend VMの実行コンテキスト(コールスタックと実行ポインタ)をオブジェクトとしてカプセル化する機能だ。
PHPの関数呼び出しは通常、Cのコールスタック(あるいはZend VMがヒープ上に構築する仮想スタック)上にフレームを積み上げる。Fiberを使うと、このスタックフレームの塊(`zend_execute_data`とコールスタック)をヒープ上に退避させ、別のFiberの実行コンテキストに切り替えることができる。

しかし、Fiber単体には「I/Oの非同期化(Epoll等によるイベントループ)」の機能がない。Fiberは単なる「関数の実行中断・再開のプリミティブ(制御構文)」に過ぎず、ブロッキングなI/O(例: 伝統的な `PDO` や `file_get_contents`)をFiber内で実行すれば、OSスレッド全体がブロックされる。

Swoole Coroutineの正体:C10Kを撃破する非同期I/Oエンジン

一方、SwooleのCoroutineは、Fiberと同様のスタック管理(Swoole独自のCレベルでのスタック管理)に加え、底層に強力なEpoll/Kqueueベースのイベントループ(Reactor)を持っている。
さらに、SwooleはPHPの標準関数(`sleep`, `file_get_contents`, `PDO`, `Redis` 等)をフックし、ブロッキングコールが発生した瞬間に自動的にコルーチンをyieldさせ、裏で非同期I/O処理を行い、完了したらレジュームする「フッキング(Hooks)」のメカニズムを備えている。

—

2. メモリとコンテキストスイッチのコスト

Zend VMのメモリ空間において、リクエスト処理は `zend_request_startup` から `zend_request_shutdown` までの一連のライフサイクルで完結する。

传统的なFPMモデルでは、1リクエスト=1プロセス(またはスレッド)であり、メモリリークはリクエスト終了時にOSによって一網打尽に回収される。しかし、SwooleやFiberを使った常駐型アプリケーション(Long-running process)では、ガベージコレクション(GC)と参照カウントの管理を誤ると、瞬く間にメモリが枯渇する。

循環参照とメモリリークの罠

FiberやSwooleのコルーチン内で巨大なデータを保持したままクロージャを記述し、そのクロージャがFiberインスタンスやグローバルなイベントループに参照され続けると、Zend VMの参照カウント(`refcount`)がゼロにならず、メモリリークを引き起こす。

特にFiberのインスタンス自体がスコープを超えて保持される場合、そのFiberのコールスタック内に存在するローカル変数もすべてヒープ上に生き残り続ける。Zend VMのGC(マーク・アンド・スウィープ)は循環参照を回収できるが、リアルタイム性が求められる常駐型プロセスにおいて、GCの頻発はレイテンシのスパイク(プチフリーズ)を招く主原因となる。

—

3. 実践リファレンスコード:Fiber vs Swoole Coroutine

百聞は一見にしかず。それぞれの特性を活かした堅牢な実装例を見ていこう。

パターンA:PHP 8.1+ Fiberによる純粋な協調タスク処理(I/Oを伴わないCPUバウンド/非同期計算向け)

Fiberは外部拡張なしで動作するため、ライブラリの依存関係を最小限に抑えたいドメインロジックに適している。

  • PHPネイティブ Fiber を用いた協調マルチタスクの例
  • 外部I/Oを含まない、またはユーザーランドのイベントループと組み合わせる前提の設計
  • /

    class TaskRunner {
    private array $fibers = [];

    public function addTask(Fiber $fiber): void {
    $this->fibers[] = $fiber;
    }

    public function run(): void {
    while (!empty($this->fibers)) {
    foreach ($this->fibers as $index => $fiber) {
    if ($fiber->isTerminated()) {
    // 終了したFiberをキューから排除(メモリ解放を促す)
    unset($this->fibers[$index]);
    continue;
    }

    if (! $fiber->isStarted()) {
    $fiber->start();
    } else {
    $fiber->resume();
    }
    }
    // 簡易的なビジーループの負荷軽減
    usleep(1000);
    }
    $this->fibers = array_values($this->fibers);
    }
    }

    // 使用例
    $runner = new TaskRunner();

    $fiber1 = new Fiber(function (): void {
    echo “Fiber 1: 開始\n”;
    Fiber::suspend(); // 処理を一時中断してメインループへ制御を返す
    echo “Fiber 1: 再開して終了\n”;
    });

    $fiber2 = new Fiber(function (): void {
    echo “Fiber 2: 開始\n”;
    Fiber::suspend();
    echo “Fiber 2: 再開して終了\n”;
    });

    $runner->addTask($fiber1);
    $runner->addTask($fiber2);

    $runner->run();

    > テクニカルリードの解説:
    > このコードの `Fiber::suspend()` は、単に実行コンテキストを親に戻しているだけである。もしこの内部でブロッキングな `file_get_contents()` などを呼べば、`run()` メソッド全体の実行が止まる。Fiber単体では非同期I/Oにならない点に細心の注意を払うこと。

    —

    パターンB:Swoole Coroutineによる超高スプレッドAPIサーバー(完全非同期I/O)

    実務で数千〜数万の同時リクエストを捌くAPIサーバーを構築する場合、Swooleのコルーチンと自動フック機能を利用するのが現時点で最も堅実な選択肢となる。

    SWOOLE_HOOK_ALL]);

    $server = new Server(“0.0.0.0”, 9501);

    $server->on(“Request”, function (Request $request, Response $response) {
    // 各リクエストは自動的にSwooleのコルーチン内で実行される

    // 例:複数の外部APIやDBクエリを並行(Concurrent)に実行する
    [$userData, $orderData] = Coroutine::parallel(2, function () {
    // ダミーの非同期I/O(実際にはここでMySQLやRedisへの非同期クエリが走る)
    Co::sleep(0.5);
    return “UserData”;
    }, function () {
    Co::sleep(0.5);
    return “OrderData”;
    });

    $response->header(“Content-Type”, “application/json”);
    $response->end(json_encode([
    ‘status’ => ‘success’,
    ‘user’ => $userData,
    ‘order’ => $orderData,
    ], JSON_THROW_ON_ERROR));
    });

    echo “Swoole HTTP Server started at http://127.0.0.1:9501\n”;
    $server->start();

    > テクニカルリードの解説:
    > `Coroutine::parallel` を使った並行処理により、500msかかる処理Aと処理Bを、直列ではなく並行に処理し、合計500ms程度でレスポンスを返すことができる(FPMであれば1秒かかるところだ)。
    > ただし、常駐型であるため、グローバル変数や静的プロパティ(`static`)へのリクエスト跨ぎの状態保持は厳禁である。これをやると、ユーザーAのデータがユーザーBに露出する致命的なセキュリティインシデント(データ混線)を引き起こす。

    —

    4. どっちを選ぶべきか?:アーキテクチャ選定の判断基準

    プロジェクトの要件定義において、FiberとSwooleのどちらを採用すべきかのマトリクスを提示する。

    | 評価項目 | PHP 8.1+ Fiber | Swoole Coroutine |
    | :— | :— | :— |
    | 実行環境 | 標準PHP(拡張モジュール不要) | 要 `swoole` 拡張モジュール(C拡張) |
    | I/O多重化 | なし(自前でイベントループ実装が必要) | あり(標準搭載のEpollベースReactor) |
    | 既存コードの流用 | 容易(ただしブロッキングに注意) | フック機能により既存ライブラリも非同期化可能 |
    | 学習コスト | 低〜中 | 中〜高(常駐型のメモリ管理知識が必須) |
    | デプロイ・運用 | FPMベース、または簡易CLI | 常駐プロセス管理(Supervisor等)が必須 |
    | 適したユースケース | キューワーカー、ドメインロジックの複雑な非同期制御、環境依存を避けたい場合 | C10K問題に直面する超高トラフィックAPI、リアルタイムチャット、マイクロサービス |

    結論としての設計方針

    1. 一般的なWebアプリケーション・管理画面・SaaSの基本API:
    今まで通り PHP-FPM + Laravel/Symfony の王道構成で十分である。無理に非同期化するコストの方が高くつく。
    2. 依存関係を増やせない制約下での複雑なワークフロー制御:
    PHP 8.1+ Fiber を採用し、ステートマシン(状態機械)や非同期タスクのパイプライン処理を美しく実装する。
    3. 極限のスループット、IoTのデータ収集基盤、リアルタイムWebSocketサーバー:
    Swoole(またはOpenSwoole / Workerman) を採用し、メモリリークとステート管理の厳格なコードレビュー体制を敷いた上で導入する。

    技術のトレンドに踊らされるな。Zend VMのメモリ構造とリクエストのライフサイクルを頭の中に描き、そのアーキテクチャが「なぜその仕様になっているのか」を説明できるエンジニアであれ。コードレビューは厳しく、しかしロジカルに行こう。

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