0Pricing
PHP Academy · レッスン

ReactPHPによるイベントループ

ReactPHPのイベントループでノンブロッキングI/Oを実行します。

「ReactPHPによるイベントループ」はCoddyKit上の無料PHP Academyレッスンです。 これはレッスン3/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはPHP Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 PHP Academyコースには全4レッスンが含まれています。

ReactPHPを一文で説明すると

ReactPHPは、単一のイベントループを中心に構築された、純粋なPHP製で依存関係のないノンブロッキングI/Oツールキットです。拡張機能は必要ありません。標準のPHPでstream_selectを使って動作し、利用可能な場合はext-event/ext-evも使用できます。これを使うと、通常のPHPだけで長時間稼働するサーバー、並行HTTPクライアント、ストリーミングパイプラインを記述できます。

グローバルループ

現代のReactPHP(v1.2以降)では、staticなReact\EventLoop\Loopファサードを通じてデフォルトのループを利用できます。どこでもループのインスタンスを渡す必要はありません。タイマーでコールバックをスケジュールすると、ループは処理すべき作業がなくなるまで実行されます。

<?php
require 'vendor/autoload.php';
use React\EventLoop\Loop;

Loop::addTimer(1.0, function () {
    echo "fired after 1 second\n";
});
echo "scheduled\n";
// Loop::run() is auto-invoked at script shutdown in v1.2+

タイマーと定期処理

ノンブロッキングなタイミング処理はsleep()に置き換わるものです。addTimerは1回だけ実行され、addPeriodicTimerは繰り返し実行されます。どちらもキャンセルして停止できます。これらはループによって駆動されるため、タイマーが待機している間も他のタスクは実行を続けます。

<?php
use React\EventLoop\Loop;

$count = 0;
$timer = Loop::addPeriodicTimer(0.5, function () use (&$count, &$timer) {
    echo 'tick ' . (++$count) . "\n";
    if ($count === 4) {
        Loop::cancelTimer($timer); // stop after 4 ticks
    }
});

非同期結果のためのPromise

ReactPHPは、後で完了する操作に対してPromiseを返します。成功時にはthen()を登録し、失敗時には2つ目のコールバックを登録します。Promiseは組み合わせられるため、コールバックを深くネストせずに結果を変換したりチェーンしたりできます。

<?php
use React\Promise\Promise;

function asyncDouble(int $n): Promise {
    return new Promise(function ($resolve) use ($n) {
        \React\EventLoop\Loop::addTimer(0.1, fn() => $resolve($n * 2));
    });
}

asyncDouble(21)
    ->then(fn($r) => printf("result: %d\n", $r))   // 42
    ->catch(fn(\Throwable $e) => printf("err: %s\n", $e->getMessage()));

非同期クライアントによる並行HTTP

最大の利点は、1つのスレッドから多数のHTTPリクエストを並行して送信できることです。React\Http\BrowserはPromiseを返し、React\Promise\all()はバッチ全体の完了を待ちます。1秒かかるリクエスト10件は、10秒ではなく約1秒で完了します。

<?php
use React\Http\Browser;
use function React\Promise\all;

$browser = new Browser();
$urls = [
    'https://httpbin.org/delay/1',
    'https://httpbin.org/delay/1',
    'https://httpbin.org/delay/1',
];
$requests = array_map(fn($u) => $browser->get($u), $urls);

all($requests)->then(function (array $responses) {
    echo 'all done: ' . count($responses) . " responses\n";
});

Fibersで同期的に読めるコードになる

コールバックチェーンは扱いにくくなります。ReactPHPはreact/asyncと統合されており、PHP 8.1のFibersを使ってPromiseをawait()し、コードを直線的に記述できます。coroutine()/async()ラッパーは、関数をFiberの内部で実行します。

<?php
use function React\Async\async;
use function React\Async\await;
use React\Http\Browser;

$main = async(function () {
    $browser = new Browser();
    $a = await($browser->get('https://httpbin.org/uuid'));
    $b = await($browser->get('https://httpbin.org/uuid'));
    echo (string) $a->getBody();
    echo (string) $b->getBody();
});
$main();

ノンブロッキングTCPサーバー

ReactPHPはサーバーで力を発揮します。SocketServerは接続を受け付けてイベントを発行します。各接続自体もノンブロッキングストリームです。1つのプロセスで数千の同時接続クライアントを処理できます。

<?php
use React\Socket\SocketServer;
use React\Socket\ConnectionInterface;

$socket = new SocketServer('127.0.0.1:8080');
$socket->on('connection', function (ConnectionInterface $conn) {
    $conn->write("Welcome\n");
    $conn->on('data', function ($data) use ($conn) {
        $conn->write('echo: ' . $data); // streamed back, non-blocking
    });
});
echo "listening on 127.0.0.1:8080\n";

数行で作るHTTPサーバー

react/httpサーバーは、ResponseまたはそのPromiseを返すコールバックでリクエストを処理します。永続プロセスであるため、依存関係を一度だけ初期化できます。ただし、リクエスト間のメモリ管理も自分で担う必要があります。

<?php
use React\Http\HttpServer;
use React\Http\Message\Response;
use Psr\Http\Message\ServerRequestInterface;
use React\Socket\SocketServer;

$http = new HttpServer(function (ServerRequestInterface $request) {
    return Response::json(['path' => $request->getUri()->getPath()]);
});
$http->listen(new SocketServer('0.0.0.0:8000'));
echo "http://0.0.0.0:8000\n";

バッファリングなしのストリーミング

ReactPHPのストリームはバックプレッシャーを実装しています。読み取り可能なストリームをpipe()で書き込み可能なストリームに接続すると、ランタイムが自動的に速度を調整します。これにより、ファイルをRAMに読み込むことなく、一定で小さなメモリ使用量のまま数ギガバイトのデータを転送できます。

<?php
use React\Stream\ReadableResourceStream;
use React\Stream\WritableResourceStream;

$source = new ReadableResourceStream(fopen('big.log', 'r'));
$dest   = new WritableResourceStream(fopen('php://stdout', 'w'));
$source->pipe($dest); // back-pressure aware, never buffers whole file

最大の禁忌:ループをブロックすること

ハンドラー内でブロッキング関数(sleep()、ブロッキングするPDO、curl_exec、URLに対するfile_get_contents)を呼び出した瞬間に、すべてが機能しなくなります。これらは単一スレッドを停止させ、すべての接続を待たせます。ReactPHP版を使用してください。遅延にはLoop::addTimerを、クエリにはclue/reactphp-*または非同期DBドライバーを、HTTPにはBrowserを使います。

<?php
use React\EventLoop\Loop;
use function React\Async\await;
use React\Promise\Deferred;

// BAD:  sleep(2);  // freezes the entire loop
// GOOD: async delay that yields control
function delay(float $s): \React\Promise\PromiseInterface {
    $d = new Deferred();
    Loop::addTimer($s, fn() => $d->resolve(null));
    return $d->promise();
}
// await(delay(2.0)); // inside a fiber/coroutine

並行性の制限

無制限のファンアウトは、ファイルディスクリプタを使い果たしたり、下流のAPIに過剰な負荷をかけたりする可能性があります。ReactPHPのclue/mq-reactは、並行数の上限を設けて処理をキューに入れます。残りを待機させながら、一度にN個のPromiseを処理します。これにより、自身の外向きの負荷にバックプレッシャーをかけられます。

<?php
use Clue\React\Mq\Queue;
use React\Http\Browser;

$browser = new Browser();
// At most 10 in-flight requests across 1000 URLs
$q = new Queue(10, null, fn($url) => $browser->get($url));

$promises = array_map(fn($u) => $q($u), $thousandUrls ?? []);
\React\Promise\all($promises)->then(function ($responses) {
    echo 'completed ' . count($responses) . " with cap of 10\n";
});

確認

並行性を損なわずに、ReactPHPのハンドラー内で2秒間待機するにはどうすればよいですか。

まとめ

ReactPHPは、標準PHPにノンブロッキングI/Oをもたらします。

  • 単一のイベントループ(Loopファサード)がタイマー、ソケット、ストリームを駆動するため、拡張機能は不要です。
  • 操作はプロミスを返します。all()はリクエストを並列実行し、react/asyncとFibersを使うと、同期処理のようなコードでawait()できます。
  • 永続的なTCP/HTTPサーバーやバックプレッシャー対応ストリームを構築し、1プロセスで数千の接続を処理できます。
  • ループを決してブロックしないでください。sleepやブロッキングI/Oは、Reactのタイマーと非同期ドライバーに置き換えます。

よくある質問

「ReactPHPによるイベントループ」レッスンは無料ですか?

はい。「ReactPHPによるイベントループ」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、PHP Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 PHP Academyコースには全4レッスンが含まれています。

「ReactPHPによるイベントループ」で何を学びますか?

ReactPHPのイベントループでノンブロッキングI/Oを実行します。 ブラウザで直接実行するハンズオンコードでPHP Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

PHP Academyを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのPHP Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン3/4です。

「ReactPHPによるイベントループ」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このPHP Academyレッスンでコードを書いて実行できますか?

はい。すべてのPHP Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. PHPの並行実行モデル
  2. PHP 8.1 Fibers
  3. ReactPHPによるイベントループ
  4. Swooleによる高性能サーバー
← PHP Academyに戻る