【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 を自動で並列利用する仕組みではありません。
この記事で扱う範囲は次のとおりです。
start・suspend・resume・throwの基本- 体験を踏まえた
Generatorとの違い - 複数 Fiber を順番に再開する最小スケジューラ
- 実装時の注意点とよくある失敗例
Fiber とは
PHP マニュアル では、Fiber は フルスタックで中断可能な関数(full-stack, interruptible functions) と説明されています。コールスタックのどこからでも Fiber::suspend() で実行を一時停止でき、あとから Fiber::resume() で再開できます。
重要な性質は次のとおりです。
| 観点 | Fiber の性質 |
|---|---|
| 並列性 | 単体では並列実行にならない。再開のタイミングは呼び出し側(スケジューラ)が決める |
| コールスタック | 各 Fiber が独自のスタックを持つ(深いネストの関数内でも中断できる) |
| 戻り値 | 中断した関数は、通常の戻り値型のまま書ける(yield のように Generator を返す必要はない) |
| 導入バージョン | PHP 8.1 以降 |
Amp や Revolt などの非同期 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
値の受け渡しは次のように整理できます。
suspend($value)に渡した値 → 外側のstart()/resume()の戻り値resume($value)に渡した値 → Fiber 内のsuspend()の戻り値
Generator との違い
前節の suspend / resume を踏まえて、PHP にもともとある yield ベースの Generator と比較します。
| 項目 | Generator | Fiber |
|---|---|---|
| スタック | スタックレス(呼び出し元が再開を担う) | フルスタック(深い呼び出しでも suspend 可能) |
| 中断の書き方 | yield(戻り値は Generator) | Fiber::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
実行順の流れは次のとおりです。
- キュー先頭のタスク A を
start→タスク A: 1を出力してsuspend→ 末尾へ戻す - キュー先頭のタスク B を
start→タスク B: 1を出力してsuspend→ 末尾へ戻す - タスク A を
resume→タスク A: 2を出力して終了 - タスク B を
resume→タスク B: 2を出力して終了
実際の I/O 待ちでは、次のような 1 サイクルが繰り返されます。
- Fiber A がソケット読み込みで
suspendする(待ち時間を他タスクへ譲る) - スケジューラが Fiber B など他のタスクを
resumeする - 読み込み完了イベントが届いたら、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のように順に実行し、値の受け渡しを確かめる。 - さらに踏み込む場合は Amp や Revolt のドキュメントで Fiber との関係を確認する。





