【実務・中級編】FiberとPHPの組み込み関数:`stream_select`や`socket_select`との連携における非同期I/Oの限界 – PHPコア・内部エンジンと高速化・並行処理の極意解析バイブル

はじめに:PHPにおける「非同期」の幻想とFiberの本質

コードレビューの場で、こんな質問を受けたことはないだろうか。
「PHP 8.1でFiberが入ったので、これでNode.jsやGoみたいにノンブロッキングな非同期I/Oが書けますよね?」

――残念ながら、答えは「半分正しく、9割は危険な勘違い」だ。

Zend VMの設計を知る者にとって、Fiberとは「非同期I/Oを実現する魔法の杖」ではない。それは単なる「ユーザーランドで制御可能なスタック付きコルーチン(Stack-ful Coroutine)」にすぎない。Fiber自身はI/Oの多重化(Multiplexing)の仕組みを持たず、カーネルに対して「今このソケットは読み取り可能か?」を問いかける能力はないのだ。

PHPで真の非同期並行処理を構築する場合、私たちは依然として `stream_select()` や `socket_select()` といったC10K問題の壁に阻まれる古のシステムコール、あるいは `ext-libuv` や `ext-ev` のようなイベントループ拡張と向き合わなければならない。

今回は、PHPの内部エンジンがI/O待ちをどう扱い、Fiberと `stream_select()` をいかにして安全に統合すべきか。その限界と、実務の戦場で生き残るための設計論をコードと共に解説する。

—

1. 内部構造の解剖:なぜ `stream_select` とFiberはそのままでは噛み合わないのか

まず、PHPの組み込み関数である `stream_select()` の挙動をZend VMとOSカーネルの視点からレイヤーを剥がして見てみよう。

`stream_select()` は、内部でC言語の `select()`(またはシステム許容範囲であれば `poll()`)をラップしている。
この関数がコールされると、PHPのプロセスはOSカーネルに対してファイル記述子(FD)の監視を依頼し、イベントが発生するまでプロセス(またはスレッド)自体がブロック(睡眠状態)に入る。

ここでFiberが存在していたとしても、Zend VMの実行コンテキストが切り替わるだけで、OSスレッド単位でのブロッキングを防ぐことはできない。もしメインのイベントループ上でブロッキングI/Oが走れば、FPMプロセス(あるいはCLIプロセス)全体が停止し、他のFiberの実行も完全に凍結される。

致命的なアンチパターン:愚直なFiber化

よくある失敗例として、次のようなコードを書く開発者がいる。

// 【危険なアンチパターン】
// Fiber内でブロッキングする stream_select を直接叩く例
$fiber = new Fiber(function () use ($socket) {
$read = [$socket];
$write = null;
$except = null;

// ここでOSレベルのブロックが発生し、プロセス全体が停滞する
// これではFiberを使う意味がない
stream_select($read, $write, $except, null);

$data = fread($socket, 8192);
echo “Received: {$data}\n”;
});

$fiber->start();

この設計の何が問題か。
1. プロセス全体のブロック: `stream_select` のタイムアウト(またはイベント発生)まで、他のタスクが一切処理できない。
2. メモリ空間(HashTable)の無駄な肥大化: Fiber生成時に確保される専用のスタック領域(zend_execute_data等のコンテキスト)が、ブロッキングによって無駄に寿命を延ばし、メモリプレッシャーを高める。

私たちが目指すべきは、「I/O待ちの検知(`stream_select`)」と「処理の実行(Fiber)」を完全にデカップリングし、独自のイベントループ(Event Loop)を介して調停するアーキテクチャである。

—

2. 実践:Fiberと `stream_select` を統合する非同期I/Oラッパーの実装

実務の現場でそのまま耐えうる、軽量かつ堅牢な非同期HTTP/TCPクライアントのコアエンジンを構築してみよう。
外部の重い非同期拡張(SwooleやReactPHP等)に頼らずとも、PHPコアの機能だけでイベントループとFiberの協調動作は実装できる。

以下のコードは、複数のソケット通信をノンブロッキングで並行処理するためのミニマムかつプロダクション・レディなイベントループ・クラスだ。

Fiber
private bool $isRunning = false;

public function __construct()
{
// タスクスケジューリング用のキュー
$this->queue = new SplQueue();
}

/

  • 非同期タスク(Fiber)を追加する

/
public function add(callable $task): void
{
$fiber = new Fiber($task);
$this->queue->enqueue($fiber);
}

/

  • ソケットの読み込み待ちをFiberのサスペンド(一時停止)として登録する

/
public function awaitRead($stream, Fiber $fiber): void
{
$key = (int) $stream;
$this->readStreams[$key] = $stream;
$this->fiberMap[$key] = $fiber;

// コントロールをイベントループ(親)に戻す
Fiber::suspend();
}

/

  • イベントループの駆動

/
public function run(): void
{
$this->isRunning = true;

// ループが空になるか、タスクが枯渇するまで回す
while ($this->isRunning) {
// 1. キューに溜まった新しいFiberを実行、または再開する
while (!$this->queue->isEmpty()) {
/ @var Fiber $fiber /
$fiber = $this->queue->dequeue();

try {
if (!$fiber->isStarted()) {
$fiber->start();
} elseif ($fiber->isSuspended()) {
$fiber->resume();
}
} catch (Throwable $e) {
// 本番環境では適切にロギングすること
fprintf(STDERR, “Fiber Exception: %s\n”, $e->getMessage());
}

// 実行後に終了していなければ、再度キューイングしない(イベントループに委譲)
if (!$fiber->isTerminated() && $fiber->isSuspended()) {
// すでに awaitRead 等でマップされている場合はスキップ
}
}

// 2. 監視対象のストリームがなければループ終了
if (empty($this->readStreams) && $this->writeStreams === []) {
if ($this->queue->isEmpty()) {
break;
}
}

// 3. stream_select による多重化(ノンブロッキングにするためタイムアウトを短く設定:例 10ms)
$read = array_values($this->readStreams);
$write = array_values($this->writeStreams);
$except = null;

$selected = @stream_select($read, $write, $except, 0, 10000);

if ($selected === false) {
// システムコールエラーやシグナル割り込み時のハンドリング
continue;
}

if ($selected > 0) {
// 読み込み可能になったストリームのFiberを再開キューに戻す
foreach ($read as $stream) {
$key = (int) $stream;
if (isset($this->fiberMap[$key])) {
$fiber = $this->fiberMap[$key];

// 監視リストから外す
unset($this->readStreams[$key], $this->fiberMap[$key]);

// 実行キューへ復帰
$this->queue->enqueue($fiber);
}
}
}
}
}
}

この実装のアーキテクチャ的優位性

1. ゼロ・ビジーウェイト(CPUの無駄遣い防止):
`stream_select()` の第4・第5引数に `0, 10000`(10ミリ秒)を指定することで、CPUコアを100%張り付かせるビジーウェイトを防ぎつつ、高頻度なI/O検知を実現している。
2. 協調的マルチタスク(Cooperative Multitasking):
I/O待ちが発生した瞬間に `Fiber::suspend()` を呼び出すことで、Zend VMの実行コンテキストを安全に退避させ、イベントループに制御を返す。

—

3. 実務での活用例:ノンブロッキングTCPクライアントの実装

上記の `EventLoop` を用いて、複数の外部API(あるいはTCPサーバー)に対して並行リクエストを飛ばすコードを見てみよう。

add(function () use ($host, $loop) {
echo “Connecting to {$host}…\n”;

// 非同期ソケットの生成(エラーハンドリングは省略せずに行うこと)
$socket = @stream_socket_client(
“tcp://{$host}:80”,
$errno,
$errstr,
5,
STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT
);

if (!$socket) {
echo “Failed to connect to {$host}: {$errstr}\n”;
return;
}

// ノンブロッキングモードに設定
stream_set_blocking($socket, false);

// イベントループに読み込み待ちを登録し、Fiberを中断
// (本来は書き込み待ちのフェーズも必要だが簡略化)
$loop->awaitRead($socket, Fiber::getCurrent());

// — ここから下がI/O完了後に再開される領域 —

// HTTPリクエストの送信
fwrite($socket, “GET / HTTP/1.1\r\nHost: {$host}\r\nConnection: close\r\n\r\n”);

// データの読み込み(ノンブロッキングなので少しずつ読むか、再度待つ必要があるが今回は簡易的に)
$response = ”;
while (!feof($socket)) {
$buffer = fread($socket, 8192);
if ($buffer === false || $buffer === ”) {
break;
}
$response .= $buffer;
}

fclose($socket);
echo “Finished request to {$host}. Length: ” . strlen($response) . “\n”;
});
}

// イベントループ始動
$loop->run();

—

4. 知っておくべき限界とメモリ管理の罠

プロフェッショナルなアーキテクトであれば、このコードやFiberを用いた非同期処理を採用する際に、以下の「限界」と「トレードオフ」をチームに周知徹底しなければならない。

① `stream_select()` のファイル記述子上限の壁(FD_SETSIZE)

PHPの `stream_select()` は、OSの `select()` システムコールに依存している場合、監視できるファイル記述子の数に上限(通常はCのマクロで定義される `FD_SETSIZE`、多くの環境で 1024)が存在する。
数千、数万の同時接続(C10K問題)をPHPでさばこうとした場合、`stream_select()` では確実に破綻する。

  • 対策: 数百を超えるスケールが要求される場合は、純粋なPHPの `stream_select` を諦め、`ext-uv`(libuvのバインディング)や `ext-ev` を採用し、Epoll/Kqueueベースのイベントループを構築すべきである。

② メモリリークとスワップアウトの危険性

Fiberは「軽量スレッド」と称されるが、それはOSスレッドと比較しての話である。PHPのFiberはコールスタックごとにZendコンテキストを持つため、数万個のFiberを同時に生存させると、それだけで数十MB〜数百MBのメモリを消費する。
また、例外発生時にFiberインスタンスがどこか(グローバルな配列など)に参照こぼれを起こすと、ガベージコレクション(GC)のサイクルから外れ、深刻なメモリリークを引き起こす。

  • 対策: 不用になったFiberやストリームリソースは、必ずマップから `unset` し、参照カウントを即座にゼロに落とす設計を徹底すること。

—

総括:PHPで非同期を設計する者への戒め

PHPにおけるFiberと `stream_select` の連携は、複雑な拡張モジュールを入れることなく非同期処理の恩恵を受けられる強力なアプローチだ。しかしそれは、OSやZend VMの挙動から目を背けて良い理由にはならない。

「なぜ今、このプロセスは止まっているのか?」
「このシステムコールはノンブロッキングで動いているか?」

この問いに常に答えられるエンジニアだけが、PHPで高スループットなモダンWebシステムを構築する資格を持つ。表面的な構文の美しさに酔うことなく、低レイヤの息吹を感じながら堅牢なコードを書き上げてほしい。

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