AMPHPの使用方法メモ (2/2)

前回からの続きです。
通常「AMPHP」の様なパッケージは、フレームワーク等に取り込まれていて、普段は存在すら気にしないで使用することが出来る様になっていると思われます。フレームワークにない独自の非同期処理を行いたい場合もあるかもしれませんので、どの様に使用したらいいのか試行錯誤しながらメモとして残します。動作環境はWindows上のVSCodeとなっています。
AMPHPをインストールします
任意の場所に空のフォルダ(amphp-test等)を作成して、VSCodeで開きます。次にCtrl + @と入力してターミナル画面を開いて、「composer require amphp/amp」と入力するとAMPHPパッケージがインストールされます。

インストールされたパッケージは、フォルダ内の「vendor」というフォルダ内に保存されます。同時に「composer.json」と「composer.lock」というファイルが作成されます。この2つのファイルさえあれば、「vendor」フォルダを削除してしまってもターミナル画面で「composer install」と入力すれば、インストールされているパッケージを復元することが出来ます。

vendorフォルダを調べると、amphp/ampパッケージは「revolt」というパッケージに依存していている様です。「VS Code Counter」というプラグインで行数を計測したところ、両方合わせて全部で4000行ほどある様です。

非同期関数を作成します
まずは最小構成の非同期関数を作成してみます。適当な名称(async01.php等)でPHPファイルを作成して、以下の様に入力します。3行目の宣言で、インストールされているすべてのComposerパッケージが使用可能になります。5行目はasync関数を短い名前で呼び出せる様に宣言しています。7行目で非同期実行したい関数を「async」関数の引数として渡してあげると、JavaScriptのプロミス的なオブジェクト(フューチャー)を返します。後はこのフューチャー($afunc)の「await」関数を呼び出すと非同期関数が実行されます。
<?php
require 'vendor/autoload.php';
use function Amp\async;
$afunc = async(function () {
echo '非同期実行されました。' . PHP_EOL;
});
$afunc->await();
実際に実行するには、エディタ画面の右上にある「Run PHP file」ボタンをクリックします。実行結果を表示するには、Ctrl + @キーを押してターミナル画面を表示して、「ターミナル」タブの左側にある「デバッグコンソール」タブをクリックすると実行結果を確認できます。

フューチャーを返す「async」関数ですが、引数が無名関数型(クロージャ)と呼ばれるタイプなので、先の例の様にコールバック形式で関数を指定する代わりに、一旦変数に代入してからasync関数に渡すことが出来ます。関数が複雑になってきた場合は、こちらの方が見やすいかもしれません。
<?php
require 'vendor/autoload.php';
use function Amp\async;
$func = function () {
echo '非同期実行されました。' . PHP_EOL;
};
$afunc = async($func);
$afunc->await();
本当に非同期(同時)に実行されているか確認するために、以下のコードを実行してみます。非同期に実行されていれば5秒程度で終わるはずです。
<?php
require 'vendor/autoload.php';
use function Amp\async;
use function Amp\delay;
use function Amp\Future\await;
$func = function () {
delay(5); // 5秒待機します
echo '非同期実行されました。' . PHP_EOL;
};
$afunc1 = async($func);
$afunc2 = async($func);
$start_time = microtime(true);
await([$afunc1, $afunc2]); // 非同期実行します
echo microtime(true) - $start_time . ' 秒で実行されました。';
実際に実行してみると、確かに非同期で実行されている様です。

混乱するので違う記述にしましたが、上のテストコードの18行目は以下の様に記述しても同じ結果になります。
await([$afunc1, $afunc2]); // 非同期実行します
↓
$afunc1->await();
$afunc2->await();
amphp/ampのソースファイルに、「await関数は処理が終了するのを待ちます。」とありますが、それぞれの関数が5秒待っているのにもかかわらず、全体的な処理が5秒で終了します。

実際にどの様に実行されているのか確認するために、以下のメッセージを追加してみます。
$afunc1->await();
echo microtime(true) - $start_time . ' 秒経過しました。' . PHP_EOL;
$afunc2->await();
echo microtime(true) - $start_time . ' 秒で実行されました。' . PHP_EOL;
実際に実行すると以下の様になりました。経過時間の表示と終了時間の表示がほぼ同じなので、最初の非同期実行「$afunc1->await()」が呼ばれたタイミングで「$afunc2」も実行されている様です。以上から「await」等の処理を待機する関数を実行すると、それまでに「async」で登録された「すべての関数」が非同期で実行されるという動作の様です。

ちなみに非同期関数を実行するには、何らかの終了を待機する関数を呼ぶ必要がある様です。フューチャーを直接関数的に呼び出すとエラーになります。

非同期関数に引数を渡します
非同期関数に引数を渡すには、以下の様に「async」関数の第2引数以降に渡してあげればいい様です。
$afunc = async($func, 'a');

文字列以外にも、数値や配列などいくつでも指定できる様です。

何らかの変換作業の様な、引数が異なる同じ処理を何回も同時に行いたい場合は、「await」関数の引数に「array_map」関数を指定して以下の様に記述できる様です。
<?php
require 'vendor/autoload.php';
use function Amp\async;
use function Amp\Future\await;
$func = function ($arg) {
echo "{$arg} を受け取りました。" . PHP_EOL;
};
$mfunc = function ($a) use ($func) {
return async($func, $a);
};
await(array_map($mfunc, ['A', 'B', 'C']));

同時実行の最大個数は、1024個までの様です。
非同期関数から戻り値を受け取ります
「await」関数の戻り値が、そのまま非同期関数の戻り値になっています。

asyncの配列を引数に指定するタイプのawait関数の戻り値は、それぞれの非同期関数の戻り値の配列になっている様です。

非同期関数のエラーをキャッチするには
try { ... } catch { ... }文で囲んであげればキャッチできる様です。
<?php
require 'vendor/autoload.php';
use function Amp\async;
$func = function () {
throw new Exception();
};
$afunc = async($func);
try {
$afunc->await();
} catch (Throwable $e) {
echo 'エラーが起きました。' . PHP_EOL;
}

非同期処理をキャンセルします
非同期処理をキャンセルするには、amphpの「DeferredCancellation」などの「Cancellation」インターフェイスを実装しているクラスのインスタンスの「getCancellation」関数の戻り値を「await」関数に渡してあげるといい様です。これらのクラスの「cancel」関数を実行すると非同期実行(await)が停止します。非同期関数がキャンセルされると「CancelledException」などの例外が発生するのでtry-catch文でキャッチする必要があります。
await関数を呼び出すと終了するまで制御が返ってこないので、参照モジュール(revolt)に含まれている「EventLoop」クラスの関数を使用して、amphpの実行ループを定期的に監視する必要がある様です。これらの関数内からキャンセル処理を実行します。以下のサンプル例ではEventLoopクラスの「delay」(指定した秒数後に引数の無名関数(クロージャ)を呼び出す関数)を使用しています。実用的には、同じクラス内にある「repeat」(指定した秒数間隔でクロージャを呼び出す関数)を使用して、定期的に状態を監視する実装になると思われます。
<?php
require 'vendor/autoload.php';
use function Amp\async;
use function Amp\delay;
use Revolt\EventLoop;
// 絶対終わらない処理
$func = function () {
while (true) {
delay(1);
echo '処理を実行中...' . PHP_EOL;
};
};
$afunc = async($func);
$cancel = new Amp\DeferredCancellation();
$watcher = function () use ($cancel) {
echo '5秒経ちましたので処理をキャンセルします。' . PHP_EOL;
$cancel->cancel();
};
try {
EventLoop::delay(5, $watcher); // 5秒後に実行されます
$afunc->await($cancel->getCancellation());
} catch (Amp\CancelledException $e) {
echo '処理がキャンセルされました。' . PHP_EOL;
}
echo "処理が終了しました。";

以上です。複雑そうなのでやはり素直にフレームワークを使用したほうがいいかもしれません。