PHPの並行処理・非同期処理で考慮すべき設計ポイント|同期処理との違いを実例で解説

PHP

PHPのプログラムは、処理を上から順番に実行していく同期的な構成が基本です。しかし、外部APIへの大量のリクエスト、メール送信、画像変換、バッチ処理などでは、複数の仕事を並行させたり、時間のかかる仕事をバックグラウンドへ回したりする設計が有効になることがあります。

並行処理や非同期処理を導入すると待ち時間を有効活用できる一方、同期処理では意識しなくてもよかった実行順序、共有データ、競合、エラー処理、再実行、タイムアウト、負荷制御などを考える必要があります。

重要なのは、非同期化そのものを目的にしないことです。処理時間やシステム要件を確認し、同期処理で十分なら単純な構成を維持し、必要な部分だけ並行化・非同期化することが保守性の高い設計につながります。

PHPにおける同期処理・並行処理・非同期処理の違い

同期処理では、ある処理が終了してから次の処理へ進む構成が一般的です。例えば「データベースから注文を取得する→メールを送る→結果を保存する」という処理なら、メール送信が終了するまで後続処理は待つことになります。

一方、並行処理では複数の仕事を同じ時間帯に進行させます。非同期処理では、時間のかかる処理の完了をその場で待たず、別の仕事を進めたり、ジョブキューなどを利用して後から処理したりする設計が可能です。

なお、並行処理と並列処理は厳密には同じ意味ではありません。並行処理は複数の仕事が進行中である状態を指し、並列処理は複数の処理が実際に同時実行されることを指します。非同期処理も必ずしも複数CPUコアによる並列実行を意味しません。

非同期化すると処理の実行順序を前提にできなくなる

同期処理の分かりやすさの一つは、原則としてコードに記述された順番を追えば処理の流れを把握できることです。複数の仕事を並行させると、それぞれが完了する順序は通信速度、外部サービス、CPU負荷などによって変化します。

例えば商品A、B、Cの情報を3つのAPIへ同時に問い合わせた場合、A→B→Cの順に開始してもC→A→Bの順で完了することがあります。結果の配列へ単純に完了順で追加すると、入力時の順序と出力時の順序が一致しないことがあります。

順番に意味がある処理では、リクエストIDや商品IDなどを結果に関連付け、処理の完了順序に依存しないデータ構造を設計することが重要です。前の処理結果が次の処理に必須なら、無理に並行化せず依存関係を明示する必要があります。

共有データでは競合状態と整合性を考える

並行処理で特に注意したいのが、複数の処理が同じデータを読み書きするケースです。実行タイミングによって結果が変わる問題は、一般に競合状態(race condition)と呼ばれます。

例えば在庫が1個の商品について、2つの処理がほぼ同時に「在庫数は1」と読み、その後それぞれ購入処理を確定すると、本来1個しかない商品を2件とも販売してしまう可能性があります。アプリケーション側で単純に「在庫が1以上なら更新」と書くだけでは、安全とは限りません。

このような処理では、データベースのトランザクション、適切なロック、条件付きUPDATE、ユニーク制約など、データストアが提供する整合性維持の仕組みを用途に応じて利用します。「同時に実行されても正しい結果になるか」という視点が、並行処理では重要です。

失敗・タイムアウト・キャンセルを個別に設計する

同期処理では途中で例外が発生すれば、その地点で一連の処理を停止させる比較的単純な設計が可能です。しかし複数処理を並行実行すると、「5件中4件は成功し、1件だけ失敗した」といった部分的成功が発生します。

例えば10個の外部APIへリクエストを送信したとき、9個が成功して1個だけタイムアウトした場合、全体を失敗として最初からやり直すのか、失敗した1件だけ再試行するのかを決めなければなりません。

そのため、非同期処理ではタイムアウト時間、最大試行回数、再試行間隔、部分的失敗の扱い、キャンセル時の処理などをあらかじめ設計しておくことが大切です。外部サービスへの再試行では、短時間に大量のリクエストを集中させない工夫も必要になります。

再実行されても壊れない冪等性が重要になる

ジョブキューを利用したバックグラウンド処理では、ワーカーが処理途中で停止した場合などに同じジョブが再実行されることがあります。このとき同じ処理を2回実行すると結果が重複する設計では問題になります。

例えば「注文完了メールを送信する」というジョブが途中で失敗したように見えて再実行された場合、実際には最初のメールが送信済みで、利用者へ同じメールが2通届く可能性があります。決済やポイント付与なら、さらに重大な問題になり得ます。

そこで、処理済みIDを保存する、データベースの一意制約を利用する、外部APIが対応していれば冪等性キーを利用するなど、同じ要求が複数回処理されても不正な重複が発生しにくい構造を検討します。

並行数を増やせば必ず高速になるわけではない

「100件の処理があるなら100件を同時実行すれば速い」とは限りません。同時実行数を増やしすぎると、CPU、メモリ、データベース接続、ファイルディスクリプタ、ネットワーク帯域などを大量に消費します。

さらに外部APIにはレート制限が設定されていることがあります。大量のリクエストを一度に送れば、HTTP 429などによって拒否され、かえって処理完了まで時間がかかる場合があります。

そのため実運用では、ワーカー数や同時接続数に上限を設けるなどのバックプレッシャーや流量制御が重要です。性能試験を行い、対象システムと外部サービスの許容量に合わせて適切な並行数を決めます。

CPU負荷の高い処理とI/O待ちでは適した方法が異なる

非同期処理の効果を考える際は、その処理が何を待っているのかを区別することも重要です。HTTP通信、データベースアクセス、ファイルI/Oなど待ち時間の長い処理では、その待ち時間に別の仕事を進めることで効率を改善できる可能性があります。

一方、画像処理や大規模な計算などCPUを継続的に使用する仕事では、非同期I/Oの仕組みだけを導入してもCPU処理そのものが高速になるわけではありません。CPU負荷の高い仕事では、別プロセスやワーカーへ処理を分散するなど、異なるアプローチが必要になる場合があります。

つまり、「遅いから非同期にする」のではなく、どこがボトルネックなのかを計測してから方式を選ぶことが重要です。

Webリクエストから重い仕事を切り離すジョブキュー

PHPのWebアプリケーションで実用性の高い非同期化の一つが、時間のかかる仕事をジョブキューへ登録し、別のワーカープロセスに処理させる方式です。メール送信、帳票生成、動画変換、大量データのインポートなどに利用できます。

例えばユーザー登録時に「DBへユーザーを保存→メールを送信→画像を生成→レスポンスを返す」とすべて同期実行すると、メールサーバーや画像処理の待ち時間まで利用者を待たせることになります。

必須の登録処理だけを完了させ、「メール送信」「画像生成」をキューへ登録してWebリクエストを終了すれば、画面の応答時間を短縮できる可能性があります。ただし、キューを採用するとワーカーの監視、失敗ジョブの管理、再試行など運用上の仕事も増える点には注意が必要です。

ログと監視は同期処理以上に重要になる

非同期処理では、ユーザーが操作した時点と実際にバックグラウンド処理が行われる時点が離れることがあります。問題発生時に通常のアクセスログだけを確認しても、原因を追跡できないケースがあります。

そこでリクエストID、ジョブID、ユーザーIDなどの相関IDをログへ含め、最初の要求からバックグラウンド処理まで追跡できるようにすると調査しやすくなります。また、キューの滞留件数、処理時間、失敗率、再試行回数なども監視対象になります。

例えば「注文処理は成功しているのに確認メールが来ない」という問い合わせに対して、注文IDから対応するメール送信ジョブを追跡できれば、どこで失敗したのかを特定しやすくなります。

同期処理と非同期処理をどう使い分けるか

すべてを非同期化する必要はありません。処理が短時間で終了し、直後に結果が必要で、同時実行による性能上の問題もないのであれば、同期処理の方が実装・テスト・障害調査を単純にできます。

一方、外部APIを複数呼び出す処理、大量の独立したI/O、レスポンス後でも構わないメール送信やファイル生成などでは、並行処理やジョブキューによる非同期化を検討する価値があります。

方式を決める際は、処理時間だけではなく、失敗時の影響、処理順序、整合性、再実行の安全性、インフラ運用の複雑さまで含めて比較することが重要です。

まとめ|PHPの非同期化では「速さ」だけでなく正しさと運用まで設計する

PHPで並行処理や非同期処理を導入すると、I/O待ち時間の有効活用やWebレスポンスの高速化などが期待できます。その一方で、実行順序が一定ではなくなり、競合状態、部分的失敗、タイムアウト、再試行、重複実行など同期処理にはない問題が目立つようになります。

特に重要なのは、共有データの整合性、冪等性、適切な同時実行数、エラー処理、監視です。ジョブキューを導入する場合には、失敗ジョブやワーカーまで運用対象になることも忘れてはいけません。

非同期処理は単なる高速化テクニックではなく、システムの実行モデルを変える設計上の選択です。まずボトルネックと要件を明確にし、同期処理で十分な部分は単純なまま維持しながら、効果の大きい部分へ限定して導入することが堅実な設計につながります。

コメント

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