Upgrade to Pro — share decks privately, control downloads, hide ads and more …

並行処理を学びGuzzleと仲良くなる

 並行処理を学びGuzzleと仲良くなる

PHPカンファレンス福岡2024の発表資料になります

※ 2024/06/23 加筆/修正しました

https://github.com/shimabox/guzzle-sample にサンプルコードをあげてあります

shimabox

June 21, 2024
Tweet

More Decks by shimabox

Other Decks in Programming

Transcript

  1. 1. 並行処理を学ぶ • プロセス / スレッド • 並行処理 / 並列処理

    • 非同期処理 並行処理を学ぶその前にもろもろ整理
  2. 1. 並行処理を学ぶ プロセス とは 待機状態へ遷移する理由 • データの到着を待つため ◦ 処理対象が無ければ無意味 •

    リソースの空きを待つため ◦ 計算できる道具が利用され ていたら待つ • 自発的に待機状態となるため ◦ タイマー処理とか ◦ リソースを専有しない
  3. 1. 並行処理を学ぶ • プロセス内で実行される軽量な実行単位 • スレッドはプロセス内のリソースを共有 ◦ 例:ファイルハンドル、データ ◦ メモリ空間を共有する

    ◦ データの競合が発生する可能性がある • 生成のオーバーヘッドはプロセスに比べて小さい • スレッド間のコンテキストスイッチも比較的高速 スレッド とは
  4. 1. 並行処理を学ぶ • 一つのCPU,コアが複数のタスクを短い時間ごと に切り替えながら実行する ◦ プロセスを切り替えたり ◦ マルチスレッドを使い高速に切り替えたり •

    タスクが「同時に」進行しているように見えるが 実際には交互に実行されている • I/Oバウンドなタスクに有効 ◦ I/O操作の待機中に他のタスクを進行させることで、 待ち時間を有効に活用する 並行処理(Concurrency) とは
  5. 1. 並行処理を学ぶ 並行処理(Concurrency) とは マルチプロセス マルチスレッド • プロセスは分かれてい るから安定 •

    プロセスの生成、コンテ キストスイッチのオー バーヘッドのコストが大 きい • コンテキストスイッチの コストは小さい • スレッドセーフを考える 必要がある • プロセスでも共有リソー スを扱う場合注意は必 要(ファイルとか)
  6. 1. 並行処理を学ぶ 並行処理(Concurrency) とは マルチプロセス マルチスレッド • マルチプロセス x マルチスレッド

    というのももちろんある • コンテキストスイッチの コストは小さい • スレッドセーフを考える 必要がある • プロセスでも共有リソー スを扱う場合注意は必 要(ファイルとか)
  7. 1. 並行処理を学ぶ 並行処理(Concurrency) とは マルチプロセス マルチスレッド • マルチプロセス x マルチスレッド

    というのももちろんある • マルチスレッドプログラ ミングとか恐怖でしかな いな
  8. 1. 並行処理を学ぶ 並行処理 / 並列処理 • 並行処理は並列処理を包 含する ◦ 並行処理が並列処理を可能

    にする • “並行処理とは、一度に多 くを扱うことです。 並列処理とは、一度に多 くを行うことです。” - Rob Pike
  9. 1. 並行処理を学ぶ • 注意したいこと ◦ プロセス/スレッド/並行/並列 といろいろ出て きたが、それぞれが独立した単語 ◦ プロセス/スレッドは実行単位を指す

    ▪ マルチ/シングルがさらにある ◦ 並行/並列は実行状態を指す ◦ 並列はマルチコアのみ 並行処理 / 並列処理
  10. 1. 並行処理を学ぶ • 注意したいこと ◦ プロセス/スレッド/並行/並列 といろいろ出て きたが、それぞれが独立した単語 ◦ プロセス/スレッドは実行単位を指す

    ▪ マルチ/シングルがさらにある ◦ 並行/並列は実行状態を指す ◦ 並列はマルチコアのみ 並行処理 / 並列処理 混ぜるな危険 😇
  11. 1. 並行処理を学ぶ • 注意したいこと ◦ 並行/並列にしたから といって必ず早くなる とは限らない ◦ オーバーヘッドはある

    ◦ アムダールの法則とい うのもあってだな 並行処理 / 並列処理 P8 図1-6 並列化による高速化の例
  12. 1. 並行処理を学ぶ イベントループ とは? • 非同期タスクの登録 ◦ 非同期操作が発生すると、その操作はイベントループによって管 理されるキュー(待機リスト)に登録される •

    タスクキュー ◦ 非同期操作が完了した後、その結果(コールバック関数)はタスク キュー(待機中のタスクリスト)に追加される ◦ たとえばPromise(成功か失敗のどちらかを保持している) • イベントループの実行 ◦ イベントループは、タスクキュー内のタスクがある限り、これら のタスクを一つずつ取り出し、実行する(デキュー) ◦ ループしながらイベントを監視しているイメージ
  13. 1. 並行処理を学ぶ イベントループ とは? • 非同期タスクの登録 ◦ 非同期操作が発生すると、その操作はイベントループによって管 理されるキュー(待機リスト)に登録される •

    タスクキュー ◦ 非同期操作が完了した後、その結果(コールバック関数)はタスク キュー(待機中のタスクリスト)に追加される ◦ たとえばPromise(成功か失敗のどちらかを保持している) • イベントループの実行 ◦ イベントループは、タスクキュー内のタスクがある限り、これら のタスクを一つずつ取り出し、実行する(デキュー) ◦ イベントをループしながら監視しているイメージ 同期的なプログラミング言語の視点から非同期処理を理解する https://speakerdeck.com/hanhan1978/understand-async-from-sync
  14. 1. 並行処理を学ぶ イベントループ とは? • 非同期タスクの登録 ◦ 非同期操作が発生すると、その操作はイベントループによって管 理されるキュー(待機リスト)に登録される •

    タスクキュー ◦ 非同期操作が完了した後、その結果(コールバック関数)はタスク キュー(待機中のタスクリスト)に追加される ◦ たとえばPromise(成功か失敗のどちらかを保持している) • イベントループの実行 ◦ イベントループは、タスクキュー内のタスクがある限り、これら のタスクを一つずつ取り出し、実行する(デキュー) ◦ イベントをループしながら監視しているイメージ 同期的なプログラミング言語の視点から非同期処理を理解する https://speakerdeck.com/hanhan1978/understand-async-from-sync 神(所)
  15. 2. Guzzleと仲良くなる PHPのHTTPクライアントでHTTPリクエストを簡単に送信でき、以下 の特徴があります。 • 同じインターフェースで同期・非同期リクエストの送信が可能 • PSR-7およびPSR-18をサポートしている • 基本的なHTTPトランスポートを抽象化(cURL、PHPストリーム、

    ソケット、非ブロッキングイベントループなどへの依存なし) • ミドルウェアシステムにより、クライアントの動作をカスタマイズ 可能 これらの機能により、複雑なHTTP操作を簡単に実行できます。 Guzzle https://github.com/guzzle/guzzle
  16. 2. Guzzleと仲良くなる PHPのHTTPクライアントでHTTPリクエストを簡単に送信でき、以下 の特徴があります。 • 同じインターフェースで同期・非同期リクエストの送信が可能 • PSR-7およびPSR-18をサポートしている • 基本的なHTTPトランスポートを抽象化(cURL、PHPストリーム、

    ソケット、非ブロッキングイベントループなどへの依存なし) • ミドルウェアシステムにより、クライアントの動作をカスタマイズ 可能 これらの機能により、複雑なHTTP操作を簡単に実行できます。 Guzzle https://github.com/guzzle/guzzle 一言も並行処理だとか謳っていない 😇
  17. 2. Guzzleと仲良くなる • 拡張モジュール ◦ pthreads ▪ マルチスレッドの機能を提供する拡張 モジュール ▪

    https://github.com/krakjoe/pthreads ◦ parallel ▪ マルチスレッドの並行処理を提供する 拡張モジュール ▪ https://github.com/krakjoe/parallel ◦ Swoole ▪ 非同期I/Oや並行処理をサポート ▪ https://www.swoole.co.uk/ ◦ PCNTL ▪ プロセス制御を提供する ▪ https://www.php.net/manual/ja/boo k.pcntl.php Guzzle https://github.com/guzzle/guzzle • ライブラリ ◦ ReactPHP ▪ 非同期I/Oを提供するライブラリで、 並行処理をサポート ▪ https://reactphp.org/ ◦ Amp ▪ 非同期I/Oをサポートするライブラリ で、コルーチンを使用して非同期プロ グラミングをサポート ▪ https://amphp.org/
  18. 2. Guzzleと仲良くなる • 拡張モジュール ◦ pthreads ▪ マルチスレッドの機能を提供する拡張 モジュール ▪

    https://github.com/krakjoe/pthreads ◦ parallel ▪ マルチスレッドの並行処理を提供する 拡張モジュール ▪ https://github.com/krakjoe/parallel ◦ Swoole ▪ 非同期I/Oや並行処理をサポート ▪ https://www.swoole.co.uk/ Guzzle https://github.com/guzzle/guzzle • ライブラリ ◦ ReactPHP ▪ 非同期I/Oを提供するライブラリで、 並行処理をサポート ▪ https://reactphp.org/ ◦ Amp ▪ 非同期I/Oをサポートするライブラリ で、コルーチンを使用して非同期プロ グラミングをサポート ▪ https://amphp.org/ これらと組み合わせる or おとなしくそれを使う
  19. 2. Guzzleと仲良くなる Guzzle https://github.com/guzzle/guzzle • バージョンは以下の通り (7.8.1) ◦ $ composer

    show guzzlehttp/guzzle name : guzzlehttp/guzzle descrip. : Guzzle is a PHP HTTP client library keywords : client, curl, framework, http, http client, psr-18, psr-7, rest, web service versions : * 7.8.1 ~
  20. 2. Guzzleと仲良くなる Guzzle サンプルその1(同期リクエスト) <?php require_once __DIR__ . '/../vendor/autoload.php'; use

    GuzzleHttp\Client; use GuzzleHttp\Psr7\Request; $client = new Client(); $res = $client->sendRequest(new Request('GET', 'http://localhost/sample/1')); echo $res->getBody();
  21. 2. Guzzleと仲良くなる Guzzle サンプルその2(同期リクエスト) <?php use Illuminate\Support\Facades\Route; Route::get('/sample/{id}', function ($id)

    { sleep(1); return response()->json([ 'id' => $id, ]); }); <?php $client = new Client(); $start = hrtime(true); $client->sendRequest('略'); $client->sendRequest('略'); $client->sendRequest('略'); $total = (hrtime(true) - $start) / 1e+9; echo "{$total} 秒" . PHP_EOL;
  22. 2. Guzzleと仲良くなる Guzzle サンプルその2(同期リクエスト) <?php use Illuminate\Support\Facades\Route; Route::get('/sample/{id}', function ($id)

    { sleep(1); return response()->json([ 'id' => $id, ]); }); <?php $client = new Client(); $start = hrtime(true); $client->sendRequest('略'); $client->sendRequest('略'); $client->sendRequest('略'); $total = (hrtime(true) - $start) / 1e+9; echo "{$total} 秒" . PHP_EOL; $ php sample2.php 3.629311876 秒
  23. 2. Guzzleと仲良くなる Guzzle サンプルその2(同期リクエスト) <?php use Illuminate\Support\Facades\Route; Route::get('/sample/{id}', function ($id)

    { sleep(1); return response()->json([ 'id' => $id, ]); }); <?php $client = new Client(); $start = hrtime(true); $client->sendRequest('略'); $client->sendRequest('略'); $client->sendRequest('略'); $total = (hrtime(true) - $start) / 1e+9; echo "{$total} 秒" . PHP_EOL; 順序に依存関係のない処理であれば、 もったいない🤔
  24. 2. Guzzleと仲良くなる Guzzle サンプルその2(同期リクエスト) <?php use Illuminate\Support\Facades\Route; Route::get('/sample/{id}', function ($id)

    { sleep(1); return response()->json([ 'id' => $id, ]); }); <?php $client = new Client(); $start = hrtime(true); $client->sendRequest('略'); $client->sendRequest('略'); $client->sendRequest('略'); $total = (hrtime(true) - $start) / 1e+9; echo "{$total} 秒" . PHP_EOL; 順序に依存関係のない処理であれば、 もったいない🤔
  25. 2. Guzzleと仲良くなる Guzzle サンプルその2(同期リクエスト) <?php use Illuminate\Support\Facades\Route; Route::get('/sample/{id}', function ($id)

    { sleep(1); return response()->json([ 'id' => $id, ]); }); <?php $client = new Client(); $start = hrtime(true); $client->sendRequest('略'); $client->sendRequest('略'); $client->sendRequest('略'); $total = (hrtime(true) - $start) / 1e+9; echo "{$total} 秒" . PHP_EOL; イメージとしてはこうしたい
  26. 2. Guzzleと仲良くなる Guzzle サンプルその3(非同期リクエスト) <?php require_once __DIR__ . '/../vendor/autoload.php'; use

    GuzzleHttp\Client; use GuzzleHttp\Promise\Utils; use GuzzleHttp\Promise\PromiseInterface; $client = new Client(); $requests = [ 'req1' => $client->requestAsync('GET', '略'), 'req2' => $client->requestAsync('GET', '略'), 'req3' => $client->requestAsync('GET', '略'), ]; $results = Utils::settle($requests)->wait(); foreach ($results as $key => $result) { if ($result['state'] === PromiseInterface::FULFILLED ) { echo "$key success" . PHP_EOL; } else { // こっちは、PromiseInterface::REJECTED echo "$key failed" . PHP_EOL; } }
  27. 2. Guzzleと仲良くなる Guzzle サンプルその3(非同期リクエスト) $client = new Client(); // リクエストを用意

    $requests = [ // $client->getAsync('http://localhost/sample/1') でもよいが // GuzzleHttp\ClientInterface で requestAsync が // 定義されているので扱いやすい 'req1' => $client->requestAsync('GET', '略'), ]; // Utils::settle() で GuzzleHttp\Promise\PromiseInterface が返る(Promiseってやつ) // PromiseInterface の wait() を呼ばないと処理は実行されない(Promiseは解決されない) $results = Utils::settle($requests)->wait(); // $resultsはPromiseが解決されているので後は好きにしてもろうて
  28. 2. Guzzleと仲良くなる Guzzle サンプルその3(非同期リクエスト) <?php require_once __DIR__ . '/../vendor/autoload.php'; use

    GuzzleHttp\Client; use GuzzleHttp\Promise\Utils; use GuzzleHttp\Promise\PromiseInterface; $client = new Client(); $requests = [ 'req1' => $client->requestAsync('GET', '略'), 'req2' => $client->requestAsync('GET', '略'), 'req3' => $client->requestAsync('GET', '略'), ]; $results = Utils::settle($requests)->wait(); foreach ($results as $key => $result) { if ($result['state'] === PromiseInterface::FULFILLED ) { echo "$key success" . PHP_EOL; } else { // こっちは、PromiseInterface::REJECTED echo "$key failed" . PHP_EOL; } } $ php sample3.php 1.2239755 秒 イメージはあってそう
  29. 2. Guzzleと仲良くなる Guzzle サンプルその3(非同期リクエスト) Utils::settle($requests) • Promiseの解決準備 • Promise ◦

    ざっくりいうとリクエスト が解決されたら、 `成功(fulfilled)` or `失敗(rejected)` を返すもの • wait() ◦ Promiseを解決するもの $client = new Client(); // ここではリクエストは投げられていない $requests = [ 'req1' => $client->requestAsync('GET', '略'), 'req2' => $client->requestAsync('GET', '略'), 'req3' => $client->requestAsync('GET', '略'), ]; // ここで、Promiseを解決している(リクエストの実行) $results = Utils::settle($requests)->wait(); // 上記は以下のようにも書ける // Promiseの解決準備 $promises = Utils::settle($requests); // Promiseの解決を待つ $results = $promises->wait();
  30. 2. Guzzleと仲良くなる Guzzle サンプルその3(非同期リクエスト) $client = new Client(); // ここではリクエストは投げられていない

    $requests = [ 'req1' => $client->requestAsync('GET', '略'), 'req2' => $client->requestAsync('GET', '略'), 'req3' => $client->requestAsync('GET', '略'), ]; // ここで、Promiseを解決している(リクエストの実行) $results = Utils::settle($requests)->wait(); // 上記は以下のようにも書ける // Promiseの解決準備 $promises = Utils::settle($requests); // Promiseの解決を待つ $results = $promises->wait(); wait() • 内部で curl_multi を使用して 非同期リクエストを実行 ◦ curl_multi系のメソッドを諸々利 用している • 複数のリクエストが並行して実 行される(I/O多重化) ◦ リクエストが完了するまで監視 • curl_multiはノンブロッキング • リクエストがすべて完了すると Promiseが解決される
  31. 2. Guzzleと仲良くなる Guzzle サンプルその3(非同期リクエスト) use GuzzleHttp\Client; use GuzzleHttp\Promise\Utils; use GuzzleHttp\Psr7\Response;

    use Psr\Http\Client\ClientExceptionInterface; $client = new Client(); $requests = [ $client->requestAsync('GET', '略')->then( function (Response $response) { // 成功時の処理 echo $response->getBody(); }, function (ClientExceptionInterface $reason) { // 失敗時の処理 // https://docs.guzzlephp.org/en/latest/quickstart.html#exceptions // GuzzleHttp\Exception\ConnectException または、 // GuzzleHttp\Exception\RequestException が実装している echo $reason; }, ), ]; Utils::settle($requests)->wait(); then() • 成功時と失敗時に呼び出される コールバックを定義できる ※ なお、curl_multi はこの記事が  詳しい • curl_multiでHTTP並行リクエストを行 うサンプル https://qiita.com/Hiraku/items/1c6 7b51040246efb4254
  32. 2. Guzzleと仲良くなる Guzzle サンプルその3(非同期リクエスト) use GuzzleHttp\Client; use GuzzleHttp\Promise\Utils; use GuzzleHttp\Psr7\Response;

    use Psr\Http\Client\ClientExceptionInterface; $client = new Client(); $requests = [ $client->requestAsync('GET', '略')->then( function (Response $response) { // 成功時の処理 sleep(1); // 処理は止まる }, function (ClientExceptionInterface $reason) { // 失敗時の処理 sleep(1); // 処理は止まる }, ), ]; Utils::settle($requests)->wait(); 注意されたし • HTTPは非同期に投げているが コールバックでブロックされる • すべてが非同期に行われるわけ ではない ※ なお、この記事が詳しい • とまれーっ うごけーっ https://qiita.com/tadsan/items/63b 8d84193498b1c6191
  33. 2. Guzzleと仲良くなる 同期処理はどうしているのか? $client->sendRequest(new Request('略')); // こうなっている // https://github.com/guzzle/guzzle/blob/7.8/src/Client.php#L132 public

    function sendRequest(RequestInterface $request): ResponseInterface { $options[RequestOptions::SYNCHRONOUS] = true; $options[RequestOptions::ALLOW_REDIRECTS] = false; $options[RequestOptions::HTTP_ERRORS] = false; return $this->sendAsync($request, $options)->wait(); }
  34. 2. Guzzleと仲良くなる 同期処理はどうしているのか? • 中でwait()を使っている • が、ここで使われるのは ◦ curl_exec •

    この呼び出しはブロッキング処 理で、リクエストが完了するま で待つ ◦ Promiseを解決していると いえば解決はしている • リクエストが完了したら結果を 返している $client->sendRequest(new Request('略')); public function sendRequest(RequestInterface $request): ResponseInterface { $options[RequestOptions::SYNCHRONOUS] = true; $options[RequestOptions::ALLOW_REDIRECTS] = false; $options[RequestOptions::HTTP_ERRORS] = false; return $this->sendAsync($request, $options)->wait(); }
  35. • curl_exec と curl_multi はどこで判断しているのか • Proxyというのがあってだな ◦ https://github.com/guzzle/guzzle/blob/7. 8/src/Handler/Proxy.php#L25

    • RequestOptions::SYNCHRONOUS が true でsend系メソッドが呼ばれていた ら ◦ Handler\CurlHandler (curl_exec) • false で呼ばれていたら ◦ Handler\CurlMultiHandler  (curl_multi) 2. Guzzleと仲良くなる 同期処理はどうしているのか? $client->sendRequest(new Request('略')); public function sendRequest(RequestInterface $request): ResponseInterface { $options[RequestOptions::SYNCHRONOUS] = true; $options[RequestOptions::ALLOW_REDIRECTS] = false; $options[RequestOptions::HTTP_ERRORS] = false; return $this->sendAsync($request, $options)->wait(); }
  36. • curl_exec と curl_multi はどこで判断しているのか • Proxyというのがあってだな ◦ https://github.com/guzzle/guzzle/blob/7. 8/src/Handler/Proxy.php#L25

    • RequestOptions::SYNCHRONOUS が true でsend系メソッドが呼ばれていた ら ◦ Handler\CurlHandler (curl_exec) • false で呼ばれていたら ◦ Handler\CurlMultiHandler  (curl_multi) 2. Guzzleと仲良くなる 同期処理はどうしているのか? $client->sendRequest(new Request('略')); public function sendRequest(RequestInterface $request): ResponseInterfac { $options[RequestOptions::SYNCHRONOUS] = true; $options[RequestOptions::ALLOW_REDIRECTS] = false; $options[RequestOptions::HTTP_ERRORS] = false; return $this->sendAsync($request, $options)->wait(); }
  37. • curl_exec と curl_multi はどこで判断しているのか • Proxyというのがあってだな ◦ https://github.com/guzzle/guzzle/blob/7. 8/src/Handler/Proxy.php#L25

    • RequestOptions::SYNCHRONOUS が true でsend系メソッドが呼ばれていた ら ◦ Handler\CurlHandler (curl_exec) • false で呼ばれていたら ◦ Handler\CurlMultiHandler  (curl_multi) 2. Guzzleと仲良くなる 同期処理はどうしているのか? $client->sendRequest(new Request('略')); public function sendRequest(RequestInterface $request): ResponseInterface { $options[RequestOptions::SYNCHRONOUS] = true; $options[RequestOptions::ALLOW_REDIRECTS] = false; $options[RequestOptions::HTTP_ERRORS] = false; return $this->sendAsync($request, $options)->wait(); } Handlerを使ってよしなに 処理をしている
  38. • curl_exec と curl_multi はどこで判断しているのか • Proxyというのがあってだな ◦ https://github.com/guzzle/guzzle/blob/7. 8/src/Handler/Proxy.php#L25

    • RequestOptions::SYNCHRONOUS が true でsend系メソッドが呼ばれていた ら ◦ Handler\CurlHandler (curl_exec) • false で呼ばれていたら ◦ Handler\CurlMultiHandler  (curl_multi) 2. Guzzleと仲良くなる 同期処理はどうしているのか? $client->sendRequest(new Request('略')); public function sendRequest(RequestInterface $request): ResponseInterface { $options[RequestOptions::SYNCHRONOUS] = true; $options[RequestOptions::ALLOW_REDIRECTS] = false; $options[RequestOptions::HTTP_ERRORS] = false; return $this->sendAsync($request, $options)->wait(); } HTTPリクエストをいい感じ に扱いやすいようにラップ してくれているんだね
  39. 2. Guzzleと仲良くなる Guzzle サンプルその4(非同期リクエスト Pool) • 非同期にリクエストを投げられるのは分かった • ただ、もう少し柔軟にリクエストを投げたい •

    実行数がよめない、大量に実行したいケースとか ◦ 一気にドーンではなく(メモリも気になるし) • たとえば100個のリクエストを同時に投げるので はなく、10個ずつ10回に分けて実行したい場合 • そこで、Poolを使う
  40. 2. Guzzleと仲良くなる Guzzle サンプルその4(非同期リクエスト Pool) <?php require_once __DIR__ . '/../vendor/autoload.php';

    use GuzzleHttp\Client; use GuzzleHttp\Pool; use GuzzleHttp\Psr7\Response; use Psr\Http\Client\ClientExceptionInterface; $client = new Client(); $requests = function ($total) use ($client) { $uri = 'http://localhost/sample/'; for ($i = 1; $i <= $total; $i++) { yield fn() => $client->requestAsync('GET', $uri . $i); } }; $pool = new Pool($client, $requests(100), [ 'concurrency' => 10, // 10個ずつリクエストを投げる(デ フォルト25回) 'fulfilled' => fn(Response $res, $i) => print("{$i} completed.\n"), // 成功時の処理 'rejected' => fn( ClientExceptionInterface $reason, $i ) => print("{$i} failed: {$reason}\n"), // 失敗時の処理 ]); $promise = $pool->promise(); $promise->wait();
  41. 2. Guzzleと仲良くなる Guzzle サンプルその4(非同期リクエスト Pool) $client = new Client(); //

    Poolから呼び出される // リクエストをGeneratorで返す $requests = function ($total) use ($client) { $uri = 'http://localhost/sample/'; for ($i = 1; $i <= $total; $i++) { yield fn() => $client->requestAsync('GET', $uri . $i); } };
  42. 2. Guzzleと仲良くなる Guzzle サンプルその4(非同期リクエスト Pool) // Poolの生成 // 送信するリクエストを管理する //

    (EachPromiseでPromiseを管理) $pool = new Pool($client, $requests(100), [ 'concurrency' => 10, // 10個ずつリクエストを投げる(デフォルト25回) // このへんthen()に似てるね 'fulfilled' => fn(Response $res, $i) => print("{$i} completed.\n"), // 成功時の処理 'rejected' => fn( ClientExceptionInterface $reason, $i ) => print("{$i} failed: {$reason}\n"), // 失敗時の処理 ]); $promise = $pool->promise(); $promise->wait();
  43. 2. Guzzleと仲良くなる Guzzle サンプルその4(非同期リクエスト Pool) // Poolの生成 // 送信するリクエストを管理する //

    (EachPromiseでPromiseを管理) $pool = new Pool($client, $requests(100), [ 'concurrency' => 10, // 10個ずつリクエストを投げる(デフォルト25回) // このへんthen()に似てるね 'fulfilled' => fn(Response $res, $i) => print("{$i} completed.\n"), // 成功時の処理 'rejected' => fn( ClientExceptionInterface $reason, $i ) => print("{$i} failed: {$reason}\n"), // 失敗時の処理 ]); $promise = $pool->promise(); $promise->wait(); レスポンスはリクエストを 投げた順番に返ってこない
  44. 2. Guzzleと仲良くなる Guzzle サンプルその4(非同期リクエスト Pool) $params = [ // 文字列が識別子となっているケース

    'foo' => ['id' => 1, '...'], 'baz' => ['id' => 2, '...'], ]; $requests = function ($params) use ($client) { $uri = 'http://localhost/sample/'; foreach ($params as $key => $param) { // yield で $key を渡すことにより、コールバックで識別子が使える yield $key => fn() => $client->requestAsync('GET', $uri . $param['id']); } }; $pool = new Pool($client, $requests($params), [ 'concurrency' => 10, 'fulfilled' => fn(Response $res, $key) => print("{$params[$key]['id']}\n"), // yieldで渡した$key 'rejected' => fn(ClientExceptionInterface $reason, $key) => [], ]);
  45. 2. Guzzleと仲良くなる Guzzle サンプルその4(非同期リクエスト Pool) $params = [ // 文字列が識別子となっているケース

    'foo' => ['id' => 1, '...'], 'baz' => ['id' => 2, '...'], ]; $requests = function ($params) use ($client) { $uri = 'http://localhost/sample/'; foreach ($params as $key => $param) { // yield で $key を渡すことにより、コールバックで識別子が使える yield $key => fn() => $client->requestAsync('GET', $uri . $param['id']); } }; $pool = new Pool($client, $requests($params), [ 'concurrency' => 10, 'fulfilled' => fn(Response $res, $key) => print("{$params[$key]['id']}\n"), // yieldで渡した$key 'rejected' => fn(ClientExceptionInterface $reason, $key) => [], ]); yield を活用すると捗る
  46. 2. Guzzleと仲良くなる Guzzle テストの題材 $client = new Client(); $params =

    [ 'foo' => ['id' => 1, '...'], 'baz' => ['id' => 2, '...'], ]; $requests = function ($params) use ($client) { $uri = 'http://localhost/sample/'; foreach ($params as $key => $param) { yield $key => fn() => $client->requestAsync('GET', $uri . $param['id']); } }; $pool = new Pool($client, $requests($params), [ 'concurrency' => 10, 'fulfilled' => fn(Response $res, $key) => print("{$params[$key]['id']}\n"), 'rejected' => fn(ClientExceptionInterface $reason, $key) => [], ]);
  47. 2. Guzzleと仲良くなる Guzzle テストの題材 readonly class GuzzleSample public function __construct(

    private ClientInterface $client, private ClientPoolFactoryInterface $poolFactory, private FulfilledHandlerInterface $fulfilledHandler, private RejectedHandlerInterface $rejectedHandler, private array $params, private int $concurrency = 10 ) {}
  48. 2. Guzzleと仲良くなる Guzzle テストの題材 readonly class GuzzleSample public function __construct(

    private ClientInterface $client, private ClientPoolFactoryInterface $poolFactory, private FulfilledHandlerInterface $fulfilledHandler, private RejectedHandlerInterface $rejectedHandler, private array $params, private int $concurrency = 10 ) {} 差し替えられるように
  49. 2. Guzzleと仲良くなる Guzzle テストの題材 readonly class GuzzleSample public function __construct(

    private ClientInterface $client, private ClientPoolFactoryInterface $poolFactory, private FulfilledHandlerInterface $fulfilledHandler, private RejectedHandlerInterface $rejectedHandler, private array $params, private int $concurrency = 10 ) {} • ClientInterface … GuzzleHttp\Clientの Interface(こっちの名前空間にしたいが割愛) • ClientPoolFactoryInterface … Pool作成ラッパーク ラスのInterface
  50. 2. Guzzleと仲良くなる Guzzle テストの題材 use GuzzleHttp\ClientInterface; use GuzzleHttp\Pool; class ClientPoolFactory

    implements ClientPoolFactoryInterface { public function factory( ClientInterface $client, $requests, array $config = [] ): Pool { return new Pool($client, $requests, $config); } }
  51. 2. Guzzleと仲良くなる Guzzle テストの題材 readonly class GuzzleSample public function __construct(

    private ClientInterface $client, private ClientPoolFactoryInterface $poolFactory, private FulfilledHandlerInterface $fulfilledHandler, private RejectedHandlerInterface $rejectedHandler, private array $params, private int $concurrency = 10 ) {}
  52. 2. Guzzleと仲良くなる Guzzle テストの題材 readonly class GuzzleSample public function __construct(

    private ClientInterface $client, private ClientPoolFactoryInterface $poolFactory, private FulfilledHandlerInterface $fulfilledHandler, private RejectedHandlerInterface $rejectedHandler, private array $params, private int $concurrency = 10 ) {} 処理成功時と失敗時のハンドラー
  53. 2. Guzzleと仲良くなる Guzzle テストの題材 <?php namespace App\Sample; use GuzzleHttp\Psr7\Response; class

    FulfilledHandler implements FulfilledHandlerInterface { public function handle(Response $res, array $reqParams): void { // Do something. } }
  54. 2. Guzzleと仲良くなる Guzzle テストの題材 <?php namespace App\Sample; use Psr\Http\Client\ClientExceptionInterface; class

    RejectedHandler implements RejectedHandlerInterface { public function handle(ClientExceptionInterface $e, array $reqParams): void { // Do something. } }
  55. 2. Guzzleと仲良くなる Guzzle テストの題材 public function call(): void { $requests

    = function ($params) { $uri = 'http://localhost/sample/'; foreach ($params as $key => $param) { yield $key => fn() => $this->client->requestAsync('GET', $uri . $param['id']); } };
  56. 2. Guzzleと仲良くなる Guzzle テストの題材 $pool = $this->poolFactory->factory( $this->client, $requests($this->params), [

    'concurrency' => $this->concurrency, 'fulfilled' => fn() => $this->fulfilledHandler->handle(), 'rejected' => fn() => $this->rejectedHandler->handle(), ] ); $promise = $pool->promise(); $promise->wait(); }
  57. 2. Guzzleと仲良くなる Guzzle テストの題材 $pool = $this->poolFactory->factory( $this->client, $requests($this->params), [

    'concurrency' => $this->concurrency, 'fulfilled' => fn() => $this->fulfilledHandler->handle(), 'rejected' => fn() => $this->rejectedHandler->handle(), ] ); $promise = $pool->promise(); $promise->wait(); }
  58. 2. Guzzleと仲良くなる Guzzle テスト class GuzzleSampleTest extends TestCase { public

    function testSample(): void { // モックハンドラーを作成 // ここで定義した順にレスポンスが返される $mock = new MockHandler([ // 第一引数でステータスコードを指定できる // 第二引数でヘッダーオプションを指定できる // 第三引数でレスポンスボディを指定できる new Response(200, [], '{"id": 1}'), // 例外をスローすることもできる new RequestException('Error', new Request('GET', 'test'), new Response(500, [], 'Internal Server Error')) ]);
  59. 2. Guzzleと仲良くなる Guzzle テスト // ハンドラーをクライアントに登録 $handlerStack = HandlerStack::create($mock); $client

    = new Client(['handler' => $handlerStack]); MockHandlerを Clientに登録するのが肝 (GuzzleはHandlerを使ってよしなに処 理をしている)
  60. 2. Guzzleと仲良くなる Guzzle テスト $params = [ 'foo' => ['id'

    => 1, 'age' => 20], 'baz' => ['id' => 2, 'age' => 50], // これはエラーレスポンスが返る ]; // FulfilledHandlerとRejectedHandlerは上で定義したレスポンスを受け取れるので // 振る舞いを検証できる (new GuzzleSample( $client, new ClientPoolFactory(), new FulfilledHandler(), new RejectedHandler(), $params ))->call();
  61. 2. Guzzleと仲良くなる Guzzle テスト $params = [ 'foo' => ['id'

    => 1, 'age' => 20], 'baz' => ['id' => 2, 'age' => 50], // これはエラーレスポンスが返る ]; // FulfilledHandlerとRejectedHandlerは上で定義したレスポンスを受け取れるので // 振る舞いを検証できる (new GuzzleSample( $client, new ClientPoolFactory(), new FulfilledHandler(), new RejectedHandler(), $params ))->call(); これだけで本物のHTTPリク エストを投げずに、ふるまい を確認することが可能
  62. 2. Guzzleと仲良くなる Guzzle テスト $params = [ 'foo' => ['id'

    => 1, 'age' => 20], 'baz' => ['id' => 2, 'age' => 50], // これはエラーレスポンスが返る ]; // FulfilledHandlerとRejectedHandlerは上で定義したレスポンスを受け取れるので // 振る舞いを検証できる (new GuzzleSample( $client, new ClientPoolFactory(), new FulfilledHandler(), new RejectedHandler(), $params ))->call(); 簡単ですね
  63. 2. Guzzleと仲良くなる Guzzle テスト $params = [ 'foo' => ['id'

    => 1, 'age' => 20], 'baz' => ['id' => 2, 'age' => 50], // これはエラーレスポンスが返る ]; // FulfilledHandlerとRejectedHandlerは上で定義したレスポンスを受け取れるので // 振る舞いを検証できる (new GuzzleSample( $client, new ClientPoolFactory(), new FulfilledHandler(), new RejectedHandler(), $params ))->call(); • リクエストクラスにきちんとパラ メータを渡せているかなどの確認は ひと工夫必要 • 本当は書きたかったが断念
  64. 3. まとめ • 並行処理を学んでいたら、プロセス, スレッド, 並列, 非同期が出てきて頭がパンク😇 • 並行処理、同時に走っているわけではなくタスク を切り替えて実行しているだけだった

    • プロセス, スレッドは処理の実行単位を指す • 並行/並列は実行状態を指す ◦ プロセス, スレッドを学ぶと必ず処理が早くなるとは 限らないことがわかる ◦ スレッドセーフ難しそう
  65. 3. まとめ • 非同期I/Oというものがあり、Guzzleは素のまま だと非同期リクエストが用いられる • curl_exec, curl_multi が使い分けられている •

    GuzzleはHTTPリクエストをいい感じに扱いやす いようにしてくれたもの • テストで本物のHTTPリクエストを叩かないよう にするのも簡単
  66. • GuzzleHttpはどのように非同期およびPromiseを実装しているのか ◦ https://qiita.com/ming_hentech/items/2daa60a33ad4811fd1e6 ◦ Guzzleの内部実装を詳しく説明している • 並行・並列とマルチスレッド・マルチプロセスの関係を整理する ◦ https://qiita.com/yukiyamamuro/items/de06878d6772ee2a1e76

    • とまれーっ うごけーっ ◦ https://qiita.com/tadsan/items/63b8d84193498b1c6191 • Guzzle Promiseを使った非同期処理によるAPIコールの高速化 ◦ https://speakerdeck.com/suzuki/guzzle-promisewoshi-tuta-fei-tong-qi-ch u-li-niyoruapikorufalsegao-su-hua 3. まとめ 参考
  67. • PHPでEventLoopを書いて非同期処理を完全に理解する ◦ https://speakerdeck.com/hanhan1978/understand-async-from-sync • curl_multiでHTTP並行リクエストを行うサンプル ◦ https://qiita.com/Hiraku/items/1c67b51040246efb4254 • PHPにおけるI/O多重化とyield

    ◦ https://www.slideshare.net/slideshow/phpioyield-phpcon2014/40136098 • Re: WebサーバーアーキテクチャとPHP実行方式の理解から始める php-fpmとはなにか? ◦ https://zenn.dev/bs_kansai/articles/3706c12408160c • 並行処理と並列処理|Goでの並行処理を徹底解剖! ◦ https://zenn.dev/hsaki/books/golang-concurrency/viewer/term 3. まとめ 参考