PHPの並行実行モデル
ブロッキングI/Oと、非同期処理が役立つ場面を理解します。
「PHPの並行実行モデル」はCoddyKit上の無料PHP Academyレッスンです。 これはレッスン1/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはPHP Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 PHP Academyコースには全4レッスンが含まれています。
PHPはデフォルトでは同期処理
従来のPHPは、1プロセスにつき1リクエストを上から下へ処理し、ブロッキングI/Oを使用します。遅いAPIに対してfile_get_contents()を実行すると、データが届くまでスクリプト全体が停止します。共有なしモデル(リクエストごとに新しいプロセスを作成し、最後に状態を破棄する方式)は非常にシンプルです。しかしその分、1つのワーカーが一度に実行できる処理は1つだけです。このレッスンでは、どこで問題になり、どこで非同期処理が役立つのかを見ていきます。
ブロッキングI/Oを測定する
経過時間の大部分が計算ではなく待機に費やされていることを確認してください。1秒かかるリクエストを3つ順番に実行すると、CPUがその間ずっとアイドル状態でも、約3秒かかります。
<?php
$start = microtime(true);
foreach (['a', 'b', 'c'] as $job) {
usleep(1_000_000); // simulate a 1s blocking network call
echo "done $job\n";
}
printf("elapsed: %.2fs\n", microtime(true) - $start); // ~3.00sCPUバウンドとI/Oバウンド
並行性が役立つのはI/Oバウンドな処理だけです。これはネットワーク、ディスク、データベースの応答を待つ時間が大部分を占める処理です。CPUバウンドな処理(ハッシュ計算、画像のリサイズ、解析)では、1スレッドですでにCPUを使い切っているため、非同期処理はスケジューリングのオーバーヘッドを増やすだけです。まず原因を特定してください。
- 遅いHTTP呼び出しやDBクエリが多数ある → 非同期処理の効果が大きい
- 数値計算のループが続く → イベントループではなく、並列処理(プロセス/スレッド)を使う
これまでのPHPのスケール方法
これまでPHPは、主にプロセスレベルの並行性によってスケールしてきました。PHP-FPMはワーカープロセスのプールを維持し、各プロセスが一度に1つのリクエストを処理し、OSがそれらを多重化します。これはリクエスト間では本当の並行性を提供しますが、1つのリクエストの内部では何も提供しません。標準のブロッキングコードを使うハンドラー内で、50個のAPI呼び出しを並列に実行することは依然としてできません。
イベントループという考え方
Async PHPではイベントループが導入されます。これは、I/O操作をノンブロッキングとして登録してから、いずれかの操作が準備できるまでstream_select()/epollで待機し、データが到着するとコールバックをディスパッチする単一スレッドです。単一のソケットの応答を待ち続けることがないため、1つのスレッドで数千のソケットを巧みに処理できます。
<?php
// Conceptual loop: poll many non-blocking streams at once
$streams = openManyNonBlockingSockets();
while ($streams) {
$read = $streams; $write = $except = [];
stream_select($read, $write, $except, null); // sleeps until ANY is ready
foreach ($read as $s) {
handleReadyStream($s); // runs only the sockets that have data now
}
}協調的であり、プリエンプティブではない
Async PHPは協調的です。タスクはI/Oポイントで自発的に制御を譲るまで実行されます。プリエンプションはないため、長いブロッキング呼び出しやタイトなCPUループがループ全体を停止させます—その結果、他のすべてのタスクが待たされます。黄金律は、イベントループのタスク内でブロッキング関数(sleep、ブロッキングするPDO、file_get_contents)を決して呼び出さないことです。ループの非同期版を使用してください。
PHPにおける3つのアプローチ
現代のPHPには、層状のツールキットがあります。
- Fibers(PHP 8.1、コア) — コールスタックを一時停止・再開するための低レベルプリミティブです。完全なフレームワークではなく、その土台となる機能です。
- ReactPHP / Amp — ストリーム(現在はFibersも)を基盤とするユーザーランドのイベントループで、拡張機能なしにノンブロッキングI/Oを実現します。
- Swoole / OpenSwoole — コルーチン、高性能サーバー、フックされたブロッキング呼び出しを提供するC拡張機能です。
戦略を選ぶ前に、実行時に何が利用可能かを検出してください。
<?php
echo 'Fibers: ' . (class_exists('Fiber') ? 'yes' : 'no'), PHP_EOL;
echo 'Swoole ext: ' . (extension_loaded('swoole') ? 'yes' : 'no'), PHP_EOL;
echo 'parallel ext: ' . (extension_loaded('parallel') ? 'yes' : 'no'), PHP_EOL;
echo 'PHP ' . PHP_VERSION, PHP_EOL;Promiseは将来の値を表す
Fibers以前は、Async PHPでは保留中の結果をPromiseとして公開していました。これは後で解決される値のプレースホルダーです。then()でコールバックを登録します。動作はしますが、ネストしたチェーンになりがちです。Fibersを使うと、ライブラリはこれを同期コードのように見えるawaitの背後に隠せます。
<?php
use React\Promise\Promise;
$promise = new Promise(function ($resolve) {
// resolved later when I/O completes
$resolve(42);
});
$promise->then(function ($value) {
echo "got $value\n";
});並行性と並列性
正確に区別してください。
- 並行性 — 1つのスレッド(イベントループ)上で多くのタスクが進行中となり、交互に実行される状態です。I/Oに適しています。
- 並列性 — 複数のコア上で多くのタスクが同時に実行される状態です(プロセス、
parallel拡張機能、Swooleのタスクワーカー)。CPU処理に必要です。
イベントループが提供するのは並行性であり、並列性ではありません。以下のスニペットはproc_openを使ったプロセスレベルの並列性を示しています。3つのワーカーが別々のOSプロセスで同時に実行されます。
<?php
$procs = [];
foreach (range(1, 3) as $i) {
// Each worker is a separate OS process -> real parallelism
$procs[] = proc_open(
"php -r 'usleep(500000); echo \"worker $i done\\n\";'",
[1 => ['pipe', 'w']], $pipes
);
}
foreach ($procs as $p) { proc_close($p); }
echo "all workers launched in parallel\n";Asyncを選ぶべきでない場合
Asyncにはコストがあります。長時間稼働するワーカーでは、PHPがリクエスト間で状態を消去しなくなるため、メモリリーク、staticキャッシュ、接続の陳腐化が問題になります。リクエストハンドラーがDBクエリを1回実行して戻るだけなら、PHP-FPMの方がシンプルで、速度も同程度です。I/Oのファンアウト、WebSockets/ロングポーリング、ストリーミング、または永続サーバーが必要な場合にAsyncを選んでください。
真の並列性:ext-parallelとプロセス
CPUバウンドのファンアウトでは、PHPで実際のOSレベルの並列処理を実行できます。parallel拡張機能はワーカースレッドを分離されたメモリ上で起動します。別の方法として、proc_open/pcntl_forkでプロセスを生成できます。イベントループとは異なり、これらは複数のコアを同時に使用します。ボトルネックが待機ではなく計算である場合に適したツールです。
<?php
// Requires ext-parallel — runs tasks on separate threads/cores
use parallel\Runtime;
$runtime = new Runtime();
$future = $runtime->run(function (): int {
$sum = 0;
for ($i = 0; $i < 1_000_000; $i++) $sum += $i; // CPU work on another core
return $sum;
});
echo $future->value(), PHP_EOL;確認
イベントループのタスク内でブロッキングするsleep(2)を呼び出すと、何が起こりますか。
まとめ
これで、PHPの並行処理についてのメンタルモデルを身につけました。
- デフォルトのPHPは同期的で、ブロッキングし、共有なしです。FPMは1つのリクエスト内ではなく、リクエスト間でスケールします。
- Asyncは、ノンブロッキングストリームを使う単一スレッドのイベントループによって、I/Oバウンドな処理を支援します。
- このモデルは協調的です。ループを決してブロックしないでください。長時間のCPU処理には、並行性ではなく並列性が必要です。
- PHPのツールキットには、Fibers(プリミティブ)、ReactPHP/Amp(ユーザーランドのループとPromise)、Swoole(コルーチン拡張機能)があります。
よくある質問
「PHPの並行実行モデル」レッスンは無料ですか?
はい。「PHPの並行実行モデル」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、PHP Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 PHP Academyコースには全4レッスンが含まれています。
「PHPの並行実行モデル」で何を学びますか?
ブロッキングI/Oと、非同期処理が役立つ場面を理解します。 ブラウザで直接実行するハンズオンコードでPHP Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。
PHP Academyを始めるのに経験は必要ですか?
事前経験は必要ありません。CoddyKitのPHP Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン1/4です。
「PHPの並行実行モデル」レッスンにはどのくらい時間がかかりますか?
ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。
このPHP Academyレッスンでコードを書いて実行できますか?
はい。すべてのPHP Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。