【PHP】Fiberの使い方|非同期処理・コルーチンの基本を実例で理解する

PHP 8.1 で導入された Fiber(ファイバー)は、非同期処理の話題でよく名前が挙がります。本記事では Fiber の基本 API、Generator との違い、複数 Fiber を束ねる最小のスケジューラ例、そしてはまりどころまで整理します。

動作の細部は PHP のバージョンによって異なる場合があります。コード例は PHP 8.1 以降 を想定します。最新の定義は PHP 公式マニュアルの Fibers の節 を参照してください。

はじめに

「PHP で非同期 I/O を扱いたい」「コルーチンという言葉は聞くが、言語組み込みの仕組みが知りたい」——こうしたニーズに応えるのが Fiber です。

ここで押さえておきたい用語は次の 2 つです。

  • コルーチン — 関数の実行を途中で止め、あとから同じ位置で再開できる仕組み
  • 協調的マルチタスク(cooperative multitasking) — 各タスクが自発的に suspend で実行を譲る方式。OS が強制的に切り替えるプリエンプティブなスレッドとは異なる

Fiber はこのコルーチンを実現する言語組み込み API ですが、OS スレッドのように CPU を自動で並列利用する仕組みではありません

この記事で扱う範囲は次のとおりです。

  • startsuspendresumethrow の基本
  • 体験を踏まえた Generator との違い
  • 複数 Fiber を順番に再開する最小スケジューラ
  • 実装時の注意点とよくある失敗例

Fiber とは

PHP マニュアル では、Fiber は フルスタックで中断可能な関数(full-stack, interruptible functions) と説明されています。コールスタックのどこからでも Fiber::suspend() で実行を一時停止でき、あとから Fiber::resume() で再開できます。

重要な性質は次のとおりです。

観点Fiber の性質
並列性単体では並列実行にならない。再開のタイミングは呼び出し側(スケジューラ)が決める
コールスタック各 Fiber が独自のスタックを持つ(深いネストの関数内でも中断できる)
戻り値中断した関数は、通常の戻り値型のまま書ける(yield のように Generator を返す必要はない)
導入バージョンPHP 8.1 以降

AmpRevolt などの非同期 I/O ライブラリは、この Fiber を土台にイベントループや I/O 待ちの切り替えを実装しています。アプリコードで直接 Fiber を書く機会は限定的ですが、ライブラリの挙動を理解するうえで基礎は押さえておくとよいです。

基本的な API

Fiber クラス の主なメソッドは次のとおりです。

メソッド役割
new Fiber(callable $callback)実行本体を渡して Fiber を生成
start(mixed $value = null)Fiber を開始。初回の suspend まで進む
Fiber::suspend(mixed $value = null)現在の Fiber を一時停止。値は start / resume / throw の戻り値になる
resume(mixed $value = null)停止中の Fiber を再開。値は直前の suspend の戻り値になる
throw(Throwable $exception)停止中の Fiber に例外を投げる(suspend の位置で捕捉される)
isStarted() / isTerminated() / isSuspended()開始・終了・停止状態の確認

Fiber::suspend() を Fiber の外から呼ぶと FiberError が投げられます(後述の失敗例)。

実装例:最小の start / suspend / resume

まずは 1 本の Fiber を止めて再開する流れです。

<?php

$fiber = new Fiber(function (): void {
    echo "開始\\n";

    // ここで一時停止。'paused' が start() の戻り値になる
    $received = Fiber::suspend('paused');

    echo "再開。受け取った値: {$received}\\n";
});

// Fiber を開始。suspend まで進み、'paused' が返る
$suspendedValue = $fiber->start();
echo "suspend から返った値: {$suspendedValue}\\n";

// 再開。'hello' が Fiber 内の $received になる
$fiber->resume('hello');

想定される出力は次のとおりです。

開始
suspend から返った値: paused
再開。受け取った値: hello

値の受け渡しは次のように整理できます。

  1. suspend($value) に渡した値 → 外側の start() / resume() の戻り値
  2. resume($value) に渡した値 → Fiber 内の suspend() の戻り値

Generator との違い

前節の suspend / resume を踏まえて、PHP にもともとある yield ベースの Generator と比較します。

項目GeneratorFiber
スタックスタックレス(呼び出し元が再開を担う)フルスタック(深い呼び出しでも suspend 可能)
中断の書き方yield(戻り値は GeneratorFiber::suspend()(関数の戻り値型はそのまま)
再開の主体Generator::send() などFiber::resume()
典型用途イテレータ、遅延評価非同期ランタイムの基盤、協調的タスク切り替え

具体例で見ると、a()b()c() と呼び出しが深いとき、c() の内部で実行を止めたい場面があります。Generator では呼び出し経路全体が yield に対応している必要があり、既存の深い関数へ後から中断点を足しにくいです。Fiber なら c() 内で Fiber::suspend() を呼ぶだけで、その Fiber のスタック上で停止できます。

マニュアルでも、深くネストした関数のなかで中断したい場合は Fiber の方が向く、と説明されています。

実装例:複数 Fiber を束ねる簡易スケジューラ

Fiber は自分では再開しません。複数タスクを切り替えるには、外側で「どの Fiber を再開するか」を決めるループ(スケジューラ)が必要です。以下は 概念を示す最小例 です(本番の非同期ランタイムほどの機能はありません)。

<?php

function runTasks(array $callables): void
{
    $queue = [];

    foreach ($callables as $callable) {
        $queue[] = new Fiber($callable);
    }

    while ($queue !== []) {
        $fiber = array_shift($queue);

        if ($fiber->isTerminated()) {
            continue;
        }

        // 未開始なら start、停止中なら resume
        if (!$fiber->isStarted()) {
            $fiber->start();
        } else {
            $fiber->resume();
        }

        if ($fiber->isSuspended()) {
            $queue[] = $fiber;
        }
    }
}

runTasks([
    function (): void {
        echo "タスク A: 1\\n";
        Fiber::suspend();
        echo "タスク A: 2\\n";
    },
    function (): void {
        echo "タスク B: 1\\n";
        Fiber::suspend();
        echo "タスク B: 2\\n";
    },
]);

想定される出力は次のとおりです。

タスク A: 1
タスク B: 1
タスク A: 2
タスク B: 2

実行順の流れは次のとおりです。

  1. キュー先頭のタスク A を startタスク A: 1 を出力して suspend → 末尾へ戻す
  2. キュー先頭のタスク B を startタスク B: 1 を出力して suspend → 末尾へ戻す
  3. タスク A を resumeタスク A: 2 を出力して終了
  4. タスク B を resumeタスク B: 2 を出力して終了

実際の I/O 待ちでは、次のような 1 サイクルが繰り返されます。

  1. Fiber A がソケット読み込みで suspend する(待ち時間を他タスクへ譲る)
  2. スケジューラが Fiber B など他のタスクを resume する
  3. 読み込み完了イベントが届いたら、Fiber A だけを resume する

ブロッキング I/O を Fiber で包んだだけでは速くならず、この 待ちのあいだに別タスクへ切り替える 判断がスケジューラ側に必要です。

実装例:Fiber::throw で例外を渡す

停止中の Fiber に例外を届けたい場合は throw() を使います。Fiber 内では suspend() の位置で例外が発生したのと同じ扱いになります。

<?php

$fiber = new Fiber(function (): void {
    try {
        Fiber::suspend('ok');
        echo "ここには到達しない\\n";
    } catch (RuntimeException $e) {
        echo "捕捉: {$e->getMessage()}\\n";
    }
});

$fiber->start();
$fiber->throw(new RuntimeException('中断地点へ例外を送る'));

想定される出力は次のとおりです。

捕捉: 中断地点へ例外を送る

注意点

ブロッキング I/O を包んでも単体では速くならない

マニュアルでも、Fiber は協調的に一時停止・再開する仕組みであり、単体でマルチスレッドのように CPU を並列利用するものではない、と説明されています。待ち時間を他タスクへ譲る スケジューラ がセットで必要です。

PHP 8.1 以降が前提

Fiber クラスは PHP 8.1 で追加されました。それ以前のバージョンでは利用できません。実行環境の php -v で確認してください。

グローバル状態への依存

複数の Fiber が同じ可変なグローバル変数やシングルトンを共有すると、再開順序で結果は変わることがあります。Fiber を使うコードでは、タスクごとに閉じたスコープで状態を持つ設計が安全です。

デバッグのしにくさ

実行が suspend / resume で分割されるため、通常の直線的なスタックトレースだけでは追いにくい場面があります。ログにタスク名を載せるなど、意図的に可観測性を足すとよいです。

よくある失敗例:Fiber の外で suspend する

Fiber::suspend()実行中の Fiber の内部 からしか呼べません。通常のトップレベルや、Fiber とは無関係な関数から呼ぶと FiberError になります。

エラーになる例

<?php

function broken(): void
{
    Fiber::suspend(); // FiberError: Must be called from a Fiber context
}

broken();

修正の考え方

中断したい処理は new Fiber(function () { ... }) のコールバック内に閉じ込め、外側は start() / resume() だけを呼ぶ形にします。既存の同期関数をそのまま suspend したい場合は、その関数を Fiber から呼び出すか、非同期ライブラリが提供するラッパーを検討します。

学び:どんなときに Fiber を意識するか

シーンFiber を直接書くか補足
通常の Web アプリ(Laravel 等)ほぼ不要フレームワークとキュー・ジョブで足りることが多い
非同期 HTTP / WebSocket クライアントライブラリ経由が現実的Amp や Revolt など内部で Fiber を使う実装が増えている
ランタイムやフレームワークの読解基礎知識として有用suspend / resume の流れが読めると設計が追いやすい

筆者の環境では、業務コードに Fiber を直書きする機会は少ない一方で、Amp や Revolt のソースを読むときに「ここで一旦譲っているのか」がわかると理解が早まりました。

まとめ

  • Fiber は PHP 8.1 以降で使える、フルスタックの協調的コルーチン用 API である。
  • suspend で渡した値は外側へ、resume で渡した値は Fiber 内の suspend の戻り値になる。
  • 単体では非同期 I/O は実現しない。複数 Fiber の再開順を決めるスケジューラがセットで必要になる。
  • Fiber::suspend() を Fiber の外から呼ぶと FiberError になる。
  • 次のアクションとして、php -v で 8.1 以降であることを確認する。
  • 記事内の 3 つのコード例を php fiber_demo.php のように順に実行し、値の受け渡しを確かめる。
  • さらに踏み込む場合は AmpRevolt のドキュメントで Fiber との関係を確認する。

Source

PHPPHP

Posted by 千原 耕司