【入門編】FiberとPHPのCLIスクリプト:長時間実行タスクのバックグラウンド実行と進捗管理 – PHPコア・内部エンジンと高速化・並行処理の極意解析バイブル

こんにちは。普段はNode.jsやGo、あるいはモダンなフロントエンドの非同期処理に慣れ親しんでいるあなたなら、PHPの「1リクエスト=1プロセス(またはスレッド)で直線的に完結する世界観」に、どこかでもどかしさを感じたことがあるかもしれません。

「なぜPHPで重いバッチ処理を回すと、CLIであっても画面がフリーズしたように固まるのか?」
「Node.jsの`async/await`のように、処理を美しくインターリーブ(細切れに実行)させられないのか?」

もしあなたがそんな壁にぶつかっているなら、安心してください。PHP 8.1で導入された Fiber(ファイバー) を使いこなすことで、PHPのプロセスモデルの限界を華麗に突破し、美しく洗練された非同期並行処理の世界を構築できるようになります。

今回は、PHPの内部エンジンが実行コンテキストをどう扱っているのかという低レイヤの視点も交えながら、CLIスクリプトにおけるFiberを活用したバックグラウンド実行と進捗管理の極意を、一緒に紐解いていきましょう。

—

1. PHPの実行モデルとFiberの正体を知る

まず、私たちが普段書いているPHPコードが、実行エンジン(Zend VM)の内部でどのように扱われているかを知ると、Fiberの凄みが一瞬で理解できます。

通常、PHPの関数呼び出しや制御構文は、コールスタック(Call Stack)というメモリ領域に積まれていきます。ある関数が別の関数を呼ぶと、スタックフレームがプッシュされ、処理が終わればポップされる。これは極めて直感的ですが、「途中で処理を一時停止して、別の処理を挟み込み、あとで元の場所から再開する」ということが、デフォルトのコールスタック構造のままではできませんでした。

コルーチン(協調的マルチタスキング)の到来

Fiberは、一言で言えば「ユーザーランドで制御できる独立したコールスタック」です。

Zend VMの視点から見ると、Fiberの内部では独自の実行コンテキスト(スタックフレームの集合)が保持されます。これにより、コードの任意の場所で `Fiber::suspend()` を呼び出すと、実行状態を完全に保持したまま親のスコープ(メインの処理)に制御を戻し、後から `Fiber::resume()` でピタッとその続きから再開できるのです。

Node.jsやGoの非同期処理がランタイムやOSによってプリエンプティブ(強制切り替え)に制御されることが多いのに対し、PHPのFiberは協調的(Cooperative)な仕組みです。つまり、「ここなら処理を譲ってもいいよ」というポイント(サスペンドポイント)をプログラマーが明示的に設計するアーキテクチャになります。ここが、PHPらしさを残したまま非同期をコントロールできる最高にエレガントなポイントですね。

—

2. 実装:CLIでの長時間タスクとノンブロッキング進捗管理

それでは、実際にコードを書いていきましょう。
今回は、「3つの重いデータ処理タスク(CSVのパースや外部APIへのバッチ送信などを想定)」をFiberを使って並行っぽく実行し、ユーザーインターフェース(CLIの画面)をブロックすることなくリアルタイムに進捗を表示するスクリプトを構築します。

ここを理解すれば、PHPの裏側でコンテキストがどう切り替わっているかが綺麗に見えてきますよ。

  • 疑似的な重い処理を行うタスクジェネレータ
  • @param string $taskName タスク名
  • @param int $totalSteps 総ステップ数
  • @return Fiber
  • /
    function createWorkflowFiber(string $taskName, int $totalSteps): Fiber
    {
    return new Fiber(function () use ($taskName, $totalSteps) {
    echo “[起動] {$taskName} の処理を開始します。\n”;

    for ($step = 1; $step <= $totalSteps; $step++) { // 1. 実際の重い処理の代わり(重いI/Oや計算をシミュレート) usleep(random_int(100000, 300000)); // 0.1秒〜0.3秒スリープ $progress = (int)(($step / $totalSteps) 100); // 2. 処理の節目で親(メインループ)へ制御を明け渡す(サスペンド) // ここで現在の進捗状況をメイン側に引き渡すのがポイントです Fiber::suspend([ 'task' => $taskName,
    ‘step’ => $step,
    ‘total’ => $totalSteps,
    ‘progress’ => $progress,
    ]);
    }

    return “{$taskName}: 完了しました!”;
    });
    }

    // — メインのイベントループ(オーケストレーション) —

    // 3つの異なるバックグラウンドタスクをFiberとして定義
    $fibers = [
    ‘Task-A’ => createWorkflowFiber(‘Task-A (顧客データ同期)’, 10),
    ‘Task-B’ => createWorkflowFiber(‘Task-B (画像アセット生成)’, 15),
    ‘Task-C’ => createWorkflowFiber(‘Task-C (ログ集計バッチ)’, 8),
    ];

    // 各Fiberを最初に一度スタートさせる(最初のサスペンドポイントまで実行)
    $results = [];
    foreach ($fibers as $key => $fiber) {
    $initialState = $fiber->start();
    // 開始直後の進捗を記録
    renderProgress($initialState);
    }

    // すべてのFiberが終了するまでループを回す(協調的マルチタスキングの核心)
    while (count($fibers) > 0) {
    foreach ($fibers as $key => $fiber) {
    // Fiberがすでに終了(terminated)しているかチェック
    if ($fiber->isTerminated()) {
    $results[$key] = $fiber->getReturn();
    unset($fibers[$key]); // 完了したものは監視対象から外す
    continue;
    }

    // 処理を再開し、次のサスペンドまで進める
    // 前回のサスペンドで渡された値を受け取る
    if ($fiber->isSuspended()) {
    $state = $fiber->resume();
    if ($state) {
    renderProgress($state);
    }
    }
    }

    // CPUを不必要に100%占有しないよう、メインループ側でも微小なウェイトを入れる
    usleep(50000);
    }

    echo “\n========================================\n”;
    echo “すべてのバックグラウンドタスクが正常に終了しました。\n”;
    foreach ($results as $taskKey => $resultMsg) {
    echo ” – {$resultMsg}\n”;
    }

    /

    • CLI上に進捗状況を綺麗に描画するヘルパー関数

    /
    ظيف renderProgress(array $state): void
    {
    $barLength = 30;
    $completed = (int)($barLength ($state[‘progress’] / 100));
    $bar = str_repeat(‘█’, $completed) . str_repeat(‘-‘, $barLength – $completed);

    // ANSIエスケープシーケンスを使って同じ行を上書き更新することも可能ですが、
    // 今回はシンプルにログ形式で出力します
    printf(
    “[%s] |%s| %3d%% (%d/%d steps)\n”,
    str_pad($state[‘task’], 25),
    $bar,
    $state[‘progress’],
    $state[‘step’],
    $state[‘total’]
    );
    }

    —

    3. このコードが内部でやっていることの解剖

    上記のスクリプトを実行すると、Task-A、Task-B、Task-Cがまるで同時に進んでいるかのように、コンソールへプログレスが出力されていきます。

    Zend VMの内部では、次のような美しいダンスが行われています。

    1. `$fiber->start()` の瞬間:
    メインのスクリプトから離れ、Fiber専用の新しいコールスタック領域が割り当てられ、ジェネレータ内のコードが実行されます。
    2. `Fiber::suspend([…])` の瞬間:
    Zend VMは現在の実行コンテキスト(ローカル変数やインストラクションポインタの位置)を保持したまま、一時停止フラグを立てて処理をメインスクリプト(`while` ループのコンテキスト)へ強制的に戻します。
    3. `$fiber->resume()` の瞬間:
    保持されていたコンテキストが復元され、サスペンドしたまさにその行(`Fiber::suspend` の返り値を受け取る瞬間)から処理が再開されます。

    従来のマルチスレッドプログラミングで頭を悩ませていた「mutex(排他制御)」や「デッドロック」といった悪夢のような問題が、この協調的(Cooperative)なシングルスレッド・イベントループの設計を採用するだけで綺麗に消え去ります。PHPのシンプルで安全な世界観をそのまま保てているのは、このためです。

    —

    4. 実務でFiberを適用する際の重要な注意点

    「よし、今日の業務から既存の重いDBクエリや外部API呼び出しをすべてFiberに置き換えよう!」と思ったあなた、ちょっと待ってください。アーキテクトとして、実務における重要なトレードオフもお伝えしておかなければなりません。

    ブロッキング関数はFiberをブロックする

    PHPの標準関数や多くのサードパーティ製ライブラリ(例えば、従来の `file_get_contents()` や通常のPDOによる同期的なDB接続など)は、ブロッキングI/Oです。
    つまり、コード上で `Fiber::suspend()` を挟まない限り、PHPの実行エンジンはOSからの応答を待つ間、そのプロセス全体で完全にフリーズしてしまいます。

    真の意味でFiberの恩恵(ノンブロッキングな並行処理)を外部I/Oで受けるためには、以下のようなアプローチが必要です。

    • Amp (amphp/amp) や ReactPHP といった、非同期I/Oランタイム(イベントループ)ベースのエコシステムとFiberを組み合わせる。
    • ソケット通信やHTTPクライアント(AmpのHTTPクライアントなど)を使い、I/O待ちが発生した瞬間に自動的にFiberがサスペンドする仕組みを構築する。

    逆に言えば、今回紹介したような「CPUバウンドな処理の細分化」や「ステップごとの進捗管理・UIの非ブロック化」であれば、純粋なPHP 8.1+のネイティブFiberだけで十分に美しく実装できます。

    —

    5. おわりに:PHPの未来を掌握する

    「PHPは古い」「リクエストごとに消えるだけの単純な言語だ」——そんなステレオタイプは、もはや過去のものになりつつあります。

    PHP 8.1のFiberや、現代の非同期エコシステムを深く理解し、Zend VMの挙動を脳内でトレースできるようになれば、あなたはもはや単なる「PHPプログラマー」ではありません。どのようなバックエンドの課題であっても、言語の特性を最大限に引き出してエレガントに解決できる「Webシステムアーキテクト」です。

    ぜひ、次のCLIバッチ開発の際には、このFiberを使ったノンブロッキングな進捗管理を試してみてください。コードの裏側でコンテキストが軽やかに切り替わる心地よさに、きっと魅了されるはずです。

    それでは、また次の極限の知見でお会いしましょう。

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