PHPのFiberで協調的な一時停止と再開を書く
Fiberは、関数の実行をどこからでも一時停止し、あとで続きから再開できるようにするPHP 8.1の機能です。
非同期処理というと自分には縁がない、という人も多いと思います。私も最初はそうでした。ただReactPHPやAmphpのようなライブラリの中身を覗くと、たいていFiberが土台に使われています。仕組みを知っておくと、そのへんのライブラリのコードが急に読めるようになったりします。
Fiberは何をしてくれるのか
普通の関数は呼ばれたら最後まで実行されて戻ってきます。途中で止めて、あとで続きから再開する、ということはできません。Fiberはこれを可能にするオブジェクトです。関数をFiberでラップして起動すると、その中でFiber::suspend()を呼んだ瞬間に処理が止まり、呼び出し元に制御が戻ります。呼び出し元は好きなタイミングでresume()を呼び、止まっていた場所から処理を再開させられます。
$fiber = new Fiber(function (): void {
echo "開始\n";
$value = Fiber::suspend('一時停止中の値');
echo "再開: {$value}\n";
});
$suspendedValue = $fiber->start();
echo "受け取った値: {$suspendedValue}\n";
$fiber->resume('再開時に渡す値');
start()はFiberの実行を開始し、内部でsuspend()が呼ばれるか関数が終わるまで戻ってきません。ポイントは、suspend()に渡した値がstart()やresume()の戻り値としてそのまま呼び出し元に届くところです。値のやり取りが双方向にできるので、単なる「中断」以上のことができます。
なぜコールバックやジェネレータではだめなのか
PHPには昔からジェネレータのyieldがあり、これも一時停止と再開ができます。ではFiberとの違いは何かというと、呼び出しの深さです。ジェネレータはyieldを含む関数自身しか止められませんが、Fiberは呼び出しスタックの奥深く、何段も関数を挟んだ先からでもsuspend()で止められます。これを「フルスタックの一時停止」と呼びます。
実務でありがたいのは、DBアクセスやHTTPリクエストのような「待ち」が発生する処理を、呼び出し元のコードをコールバック地獄にすることなく、あたかも同期処理のように書けることです。実際にはこの仕組みの上にイベントループを組み合わせて初めて非同期I/Oとして機能します。Fiber単体は「止めて再開する」仕組みを提供するだけで、勝手に並行実行してくれるわけではありません。
ハマりどころは例外の扱い
Fiber内で例外が投げられて捕まらなかった場合、その例外はstart()やresume()を呼んだ側に伝播します。逆に、外側から$fiber->throw($exception)を呼ぶと、Fiberの内側のsuspend()がその場で例外を投げたことになります。ここを知らずに書くと、try-catchの位置がずれて例外を取りこぼすことがあります。
$fiber = new Fiber(function (): void {
try {
Fiber::suspend();
} catch (\RuntimeException $e) {
echo "内部でキャッチ: {$e->getMessage()}\n";
}
});
$fiber->start();
$fiber->throw(new \RuntimeException('外から投げ込まれた例外'));
もう一つ気をつけたいのが、Fiberの外でFiber::suspend()を呼ぶとエラーになる点です。suspend()はあくまで「今動いているFiberの中」でしか呼べません。トップレベルのスクリプトから直接呼ぶとFiberErrorになります。
状態はどうやって確認する?
FiberオブジェクトにはisStarted()、isRunning()、isSuspended()、isTerminated()といった状態確認メソッドが揃っています。イベントループを自作する場合、これらを見て「まだ再開していいFiberかどうか」を判断することになります。終了時の戻り値はgetReturn()で取得できますが、Fiberが終了する前に呼ぶと例外になるので、isTerminated()で確認してから呼ぶのが安全です。
まとめ
Fiberは、それ単体で非同期処理をもたらす魔法の機能ではなく、あくまで「関数の実行をどこからでも中断・再開できる」という低レベルな部品です。普段の業務コードで直接Fiberを書く機会は少ないかもしれませんが、Amphpなどの非同期ライブラリを使うときに中で何が起きているかを理解できると、詰まったときの読解が格段に楽になります。まずは仕組みだけ頭に入れておいて、必要になったら思い出す、くらいの距離感でいいと思っています。
よくある質問
Q. FiberとGeneratorはどちらを使えばいいですか?
A. 単純に値を順番に生成したいだけならGeneratorで十分です。呼び出しスタックの深い場所から中断・再開したい場合や、双方向に値をやり取りしたい場合はFiberが向いています。
Q. Fiberを使うと自動的にマルチスレッドになりますか?
A. なりません。Fiberはあくまで単一スレッド内での協調的な一時停止・再開の仕組みで、実際に複数の処理を並行して進めるにはイベントループなど別の仕組みと組み合わせる必要があります。
Q. Fiberはどのバージョンから使えますか?
A. PHP 8.1で追加されました。それより前のバージョンでは使えません。