PHPで非同期処理を実装する方法について

採用はこちら

PHPの非同期処理とは、ある処理の完了を単純に待ち続けるのではなく、待ち時間を利用して別の処理を進めたり、時間のかかる処理をWebリクエストとは別の仕組みに任せたりする方法です。

PHPは一般的なWebアプリケーションでは、リクエストを受け取ってからPHPコードを実行し、処理が完了するとレスポンスを返す流れで動作します。

そのため、メール送信や外部APIへのアクセス、大量データの集計などに時間がかかると、その分だけユーザーへのレスポンスも遅くなる場合があります。

このような問題を解決するために利用されるのが、非同期処理やバックグラウンド処理です。

ただし、「PHPの非同期処理」と呼ばれるものには複数の種類があります。

主な方法としては、次のようなものがあります。

  • curl_multi_exec()によるHTTP通信の並行処理
  • fastcgi_finish_request()によるレスポンス後の処理
  • proc_open()などを利用した別プロセスの起動
  • ジョブキューとWorkerを利用したバックグラウンド処理
  • ReactPHPやAmpなどを利用したイベント駆動型処理
  • Fiberを利用した処理の中断と再開

重要なのは、すべてを同じ「非同期処理」と考えるのではなく、何を非同期化したいのかによって実装方法を選ぶことです。

目次

PHPで非同期処理を利用するメリット

PHPで非同期処理を取り入れると、Webアプリケーションのレスポンス改善やサーバー処理の効率化につながります。

ユーザーの待ち時間を短縮できる

例えば、会員登録後にウェルカムメールを送信するとします。

同期処理では、次のような流れになります。

registerUser();

sendWelcomeMail();

echo '登録が完了しました';

この場合、sendWelcomeMail()が完了するまでユーザーにはレスポンスが返りません。

メールサーバーとの通信に時間がかかれば、その分だけユーザーの待ち時間も長くなります。

メール送信をジョブキューへ登録し、別のWorkerに任せれば、ユーザーには登録完了後すぐにレスポンスを返すことができます。

複数の外部APIを効率よく呼び出せる

複数の外部APIを順番に呼び出す場合、それぞれの通信が完了するまで待機する必要があります。

$result1 = requestApi1();
$result2 = requestApi2();
$result3 = requestApi3();

例えば、それぞれのAPI通信に2秒かかれば、単純な逐次処理では合計で約6秒かかります。

API同士に依存関係がなければ、複数のHTTP通信を並行して進めることで全体の待ち時間を短縮できる可能性があります。

このような用途では、curl_multi_exec()などが利用できます。

重い処理をWebリクエストから分離できる

次のような処理は、Webリクエストの中ですべて実行するとレスポンスの遅延やタイムアウトにつながる場合があります。

  • 大量メール送信
  • CSV生成
  • PDF生成
  • 画像変換
  • 動画変換
  • 大量データ集計
  • 外部サービスとの同期
  • 検索インデックスの更新

このような処理は、ジョブキューへ登録して別のWorkerで実行する方法が適しています。

PHPで非同期処理を実装する主な方法

PHPで非同期処理を実装する方法は複数あります。

用途ごとに特徴が異なるため、それぞれの違いを理解しておきましょう。

curl_multi_exec()でHTTP通信を並行処理する

curl_multi_exec()は、複数のcURLリクエストを並行して処理するためのPHP標準機能です。

複数のWeb APIやWebサイトへ同時にHTTPリクエストを送信したい場合に利用できます。

curl_multi_exec()の基本的な使い方

例えば、3つのURLへ並行してアクセスする場合は次のように記述できます。

<?php

$urls = [
    'https://example.com/api/1',
    'https://example.com/api/2',
    'https://example.com/api/3',
];

$multiHandle = curl_multi_init();

$handles = [];

foreach ($urls as $url) {
    $ch = curl_init();

    curl_setopt_array($ch, [
        CURLOPT_URL => $url,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 10,
    ]);

    curl_multi_add_handle($multiHandle, $ch);

    $handles[] = $ch;
}

do {
    $status = curl_multi_exec($multiHandle, $running);

    if ($running) {
        curl_multi_select($multiHandle);
    }
} while ($running && $status === CURLM_OK);

$results = [];

foreach ($handles as $ch) {
    $results[] = curl_multi_getcontent($ch);

    curl_multi_remove_handle($multiHandle, $ch);
    curl_close($ch);
}

curl_multi_close($multiHandle);

print_r($results);

この方法では、1つ目のHTTP通信が完了してから2つ目を開始するのではなく、複数の通信を並行して進められます。

curl_multi_exec()が向いているケース

curl_multi_exec()は、主に次のような用途に向いています。

  • 複数のWeb APIへアクセスする
  • 複数URLからデータを取得する
  • 複数サービスへHTTPリクエストを送る
  • ネットワーク待ち時間を短縮したい

一方で、curl_multi_exec()はPHPプログラムそのものをバックグラウンドへ移動する機能ではありません。

そのため、メール送信や画像処理、大量データ集計などをWebリクエストから完全に切り離したい場合は、ジョブキューなど別の方法を検討します。

fastcgi_finish_request()でレスポンス後に処理する

PHP-FPM環境では、fastcgi_finish_request()を利用する方法があります。

この関数を呼び出すと、ユーザーへのレスポンスを送信してリクエストを終了させた後も、PHP側で後続処理を続けることができます。

fastcgi_finish_request()の基本例

<?php

echo json_encode([
    'status' => 'success',
    'message' => '受付が完了しました',
]);

if (function_exists('fastcgi_finish_request')) {
    fastcgi_finish_request();
}

// ユーザーへのレスポンス送信後に実行する処理

file_put_contents(
    __DIR__ . '/log.txt',
    '後続処理が完了しました' . PHP_EOL,
    FILE_APPEND
);

この方法を利用すると、ユーザーには先にレスポンスを返し、その後でログ記録などを実行できます。

fastcgi_finish_request()は完全なバックグラウンド処理ではない

fastcgi_finish_request()を利用する際に注意したいのは、PHP処理そのものが別のWorkerへ移動するわけではないことです。

あくまで、クライアントへのレスポンスを先に終了するための仕組みです。

その後もPHPコードは実行されるため、数分かかる処理やCPU負荷の高い処理を大量に実行する用途には適していません。

短時間の後続処理には便利ですが、本格的なバックグラウンド処理にはジョブキューを利用するほうが管理しやすくなります。

proc_open()で別プロセスを起動する

PHPから別のプログラムやPHPスクリプトを起動したい場合は、proc_open()を利用できます。

proc_open()は外部プロセスを起動し、標準入力・標準出力・標準エラー出力などを制御できる関数です。

proc_open()の基本例

例えば、別のPHPスクリプトを起動する場合は次のように記述できます。

<?php

$command = [
    PHP_BINARY,
    __DIR__ . '/worker.php',
];

$descriptorSpec = [
    0 => ['pipe', 'r'],
    1 => ['file', '/dev/null', 'a'],
    2 => ['file', '/dev/null', 'a'],
];

$process = proc_open(
    $command,
    $descriptorSpec,
    $pipes
);

echo '処理を開始しました';

別ファイルのworker.phpには、実行したい処理を記述します。

<?php

sleep(10);

file_put_contents(
    __DIR__ . '/result.txt',
    '処理が完了しました'
);

proc_open()だけでジョブキューになるわけではない

proc_open()は別プロセスを起動できますが、それだけで本格的なバックグラウンド処理基盤になるわけではありません。

例えば、ジョブキューでは通常、次のような機能が必要になります。

  • 処理の再試行
  • 失敗ジョブの管理
  • Worker監視
  • 実行状態の管理
  • タイムアウト
  • 重複実行防止

proc_open()単体には、こうしたジョブ管理機能はありません。

そのため、簡単な外部プロセスの起動には利用できますが、大規模なWebアプリケーションでは専用のジョブキューを利用したほうが適しています。

proc_close()はプロセス終了を待つ

proc_open()と合わせて利用される関数にproc_close()があります。

ただし、proc_close()はプロセスが終了するまで待機します。

例えば、

$process = proc_open(
    $command,
    $descriptorSpec,
    $pipes
);

proc_close($process);

とした場合、子プロセスが終了するまで処理が戻ってこないため、単純な「完全非同期処理」として利用できるわけではありません。

ジョブキューでバックグラウンド処理を実装する

本格的なWebアプリケーションで時間のかかる処理を非同期化する場合は、ジョブキューを利用する方法が一般的です。

ジョブキューの基本的な仕組み

構成は次のようになります。

ユーザー
  ↓
Webアプリケーション
  ↓
QueueへJobを登録
  ↓
すぐにレスポンス

-------------------

Worker
  ↓
QueueからJobを取得
  ↓
処理を実行

Webアプリケーション側では、重い処理そのものを実行しません。

代わりに、

ユーザーID100へメールを送信する

といった「実行すべき処理」をジョブとしてキューへ登録します。

その後、別のWorkerがジョブを取得して処理します。

Redisを利用した簡単なキューのイメージ

Redisなどを利用して、簡単なキューを作ることもできます。

例えば、Web側でジョブを登録します。

$job = json_encode([
    'type' => 'send_mail',
    'user_id' => 100,
]);

$redis->rPush('jobs', $job);

Worker側では、ジョブを取得して処理します。

while (true) {
    $job = $redis->blPop(['jobs'], 0);

    $data = json_decode($job[1], true);

    if ($data['type'] === 'send_mail') {
        sendMail($data['user_id']);
    }
}

ただし、これは仕組みを理解するための簡略化した例です。

本番環境では、単純にRedisのリストへ登録するだけでなく、リトライや失敗管理、ジョブの状態管理などを実装する必要があります。

ジョブキューで必要になる機能

実際のシステムでは、次のような仕組みを検討します。

  • 最大再試行回数
  • タイムアウト
  • 失敗ジョブの保存
  • 優先度
  • 実行履歴
  • Worker監視
  • エラー通知
  • 重複実行対策
  • ジョブの冪等性

特に重要なのが、同じジョブが複数回実行されても問題が起きないようにする「冪等性」です。

Laravelで非同期処理を実装する

Laravelを使用している場合は、標準のQueue機能を利用できます。

メール送信や外部API処理、画像処理などをバックグラウンドで実行する場合に便利です。

Jobを作成する

Laravelでは、次のコマンドでJobクラスを作成できます。

php artisan make:job SendMailJob

例えば、次のようなJobを作ります。

<?php

namespace App\Jobs;

use Illuminate\Contracts\Queue\ShouldQueue;

class SendMailJob implements ShouldQueue
{
    public function __construct(
        public int $userId
    ) {
    }

    public function handle(): void
    {
        // メール送信処理
    }
}

Jobをキューへ登録する

Webリクエスト側では、次のようにJobを登録します。

SendMailJob::dispatch($userId);

return response()->json([
    'message' => 'メール送信を受け付けました',
]);

Workerは次のように起動できます。

php artisan queue:work

この構成にすることで、Webリクエストと実際のメール送信処理を分離できます。

ReactPHPやAmpを利用して非同期処理する

大量のネットワークI/Oを非同期で扱いたい場合は、ReactPHPやAmpなどのライブラリを利用する方法があります。

これらのライブラリでは、イベントループを利用してI/O待ち時間に別の処理を進めます。

イベントループの考え方

通常の同期処理は次のような流れになります。

APIへリクエスト
↓
レスポンスを待つ
↓
次の処理

イベント駆動型では、次のように処理できます。

API Aへリクエスト
↓
待機

その間に
API Bへリクエスト
↓
待機

API Aの結果を処理

ネットワーク通信の待ち時間を有効活用できるため、大量のI/Oを扱うサービスと相性があります。

Fiberを利用する

PHP 8.1以降ではFiberを利用できます。

Fiberは、PHPコードの実行を途中で中断し、後から再開できる仕組みです。

Fiberの基本例

$fiber = new Fiber(function (): void {
    echo '開始' . PHP_EOL;

    Fiber::suspend();

    echo '再開' . PHP_EOL;
});

$fiber->start();

echo 'メイン処理' . PHP_EOL;

$fiber->resume();

この例では、Fiber::suspend()でFiberの処理を一時停止し、resume()によって再開しています。

Fiberだけでは並列処理にならない

Fiberについて特に注意したいのは、Fiberを利用しただけで処理が別CPUコアで並列実行されるわけではないことです。

Fiberは処理を中断・再開するための仕組みであり、マルチスレッド機能ではありません。

そのため、外部APIへのアクセスを自動的に並列化したり、CPU処理を自動的に高速化したりするものではありません。

主に非同期ライブラリやイベントループを構築するための基盤として利用されます。

exec()でバックグラウンド実行する方法

LinuxやUnix系の環境では、exec()を利用してコマンドをバックグラウンド実行する方法が紹介されることがあります。

例えば、次のようなコードです。

exec(
    'php worker.php > /dev/null 2>&1 &'
);

最後の&によって、シェル上ではバックグラウンド実行されます。

exec()は環境依存が大きい

この方法はOSやシェルへの依存が大きいため注意が必要です。

特に、

> /dev/null 2>&1 &

という書き方はUnix系環境を前提としています。

Windowsでは同じコードをそのまま利用できません。

また、exec()にはジョブの状態管理や再試行、失敗管理といった機能はありません。

そのため、本格的なWebサービスの非同期処理では、ジョブキューの代替として安易に利用しないほうがよいでしょう。

非同期処理と並列処理の違い

PHPで非同期処理を理解する際は、「同期処理」「非同期処理」「並列処理」の違いを把握しておくことが重要です。

同期処理

同期処理では、一つの処理が終わるまで次の処理を開始しません。

処理A
↓
完了
↓
処理B
↓
完了
↓
処理C

実装は分かりやすいものの、外部通信などで待ち時間が発生すると、その間は次の処理へ進めません。

非同期処理

非同期処理では、ある処理の待ち時間中に別の処理を進めることができます。

処理Aを開始
↓
待機

その間に
処理Bを実行

↓
処理Aが完了

主にHTTP通信やデータベース通信など、I/O待ちが発生する処理で効果を得やすくなります。

並列処理

並列処理では、複数の処理を物理的または論理的に同時実行します。

CPU1 → 処理A

CPU2 → 処理B

CPU3 → 処理C

画像変換や大量計算などのCPU負荷が高い処理では、複数プロセスや複数Workerを利用する方法が有効です。

非同期処理と並列処理は似ていますが、必ずしも同じものではありません。

PHPで非同期処理を実装するときの注意点

非同期処理を導入すれば、必ずアプリケーションが高速になるわけではありません。

設計上の注意点もあります。

処理同士に依存関係がないか確認する

例えば、次のような処理では順序が重要です。

ユーザー作成
↓
作成したユーザーIDを取得
↓
プロフィール登録

後続処理が前の処理結果に依存する場合、単純に並行実行することはできません。

一方、

API A

API B

API C

のように各処理が独立していれば、並行処理による高速化を期待できます。

エラー処理を設計する

同期処理では、その場で例外を処理できます。

try {
    process();
} catch (Throwable $e) {
    // エラー処理
}

しかし、ジョブキューではユーザーへのレスポンスを返した後にエラーが発生する可能性があります。

Webリクエスト
↓
Job登録
↓
レスポンス

数秒後

Worker
↓
Job実行
↓
エラー

そのため、次のような仕組みが必要になります。

  • エラーログ
  • リトライ
  • Failed Job管理
  • エラー通知
  • Worker監視

二重実行に備える

ジョブキューでは、ネットワーク障害やWorker停止などによって同じジョブが再実行される場合があります。

そのため、同じ処理を複数回実行しても問題が起こらないように設計することが重要です。

例えば決済処理の場合、単純に再実行すると二重課金につながる可能性があります。

そこで、

order_id = 12345

などの一意なIDを使って、

この注文はすでに決済済みか

を確認してから処理する方法があります。

このような設計を冪等性と呼びます。

データベーストランザクションとのタイミングに注意する

データベーストランザクションとジョブキューを併用する場合は、Jobを登録するタイミングにも注意が必要です。

例えば、

DB::beginTransaction();

$user = createUser();

SendMailJob::dispatch($user->id);

DB::commit();

という処理では、Workerが非常に早くJobを取得すると、データベースのコミット前にJobが実行される可能性があります。

そのため、利用しているフレームワークに「トランザクション完了後にJobを投入する仕組み」がある場合は、それを利用すると安全です。

PHPの非同期処理はどの方法を選べばよい?

非同期処理の方法は、目的によって選ぶのが重要です。

複数のHTTP通信を同時に実行したい場合

複数の外部APIなどへ同時にアクセスしたい場合は、

curl_multi_exec()

が候補になります。

HTTP通信の待ち時間を短縮する目的に向いています。

レスポンスだけ先に返したい場合

PHP-FPM環境で短時間の後続処理を実行したい場合は、

fastcgi_finish_request()

を利用できます。

ただし、重い処理を別Workerへ移動する仕組みではありません。

重い処理をバックグラウンドで実行したい場合

次のような処理では、ジョブキューとWorkerを利用する方法が適しています。

  • メール送信
  • CSV生成
  • PDF生成
  • 画像処理
  • 動画処理
  • 大量データ集計

Webリクエストから処理を分離できるため、ユーザーへのレスポンスを高速化しやすくなります。

外部プログラムを起動したい場合

別のPHPスクリプトや外部コマンドを実行したい場合は、

proc_open()

などを利用できます。

ただし、本格的なジョブ管理には向かないため、用途を限定して使用するのが適切です。

PHPの非同期処理の実装方法を比較

代表的な方法を整理すると、次のようになります。

目的主な方法
複数のHTTP通信を並行実行するcurl_multi_exec()
レスポンスを先に返すfastcgi_finish_request()
外部プロセスを起動するproc_open()
重い処理をバックグラウンド実行するジョブキュー+Worker
Laravelでバックグラウンド処理するLaravel Queue
イベント駆動型の処理を実装するReactPHP・Amp
処理を中断・再開するFiber

PHPで非同期処理を実装するときは、「非同期にしたい」というだけで方法を決めるのではなく、処理の性質から選択することが重要です。

PHPで非同期処理を実装する基本的な構成例

例えば、会員登録後にメールを送信するシステムを考えてみます。

同期処理では、

ユーザー登録
↓
メール送信
↓
レスポンス

となるため、メール送信に時間がかかるとレスポンスも遅くなります。

ジョブキューを利用すると、

① ユーザーから登録リクエスト
↓
② データベースへユーザーを登録
↓
③ Queueへメール送信Jobを登録
↓
④ ユーザーへ登録完了レスポンス

----------------------------

⑤ WorkerがJobを取得
↓
⑥ メールを送信
↓
⑦ 処理完了

という構成にできます。

Web側のイメージは次のようになります。

$user = createUser($request);

$queue->push([
    'type' => 'welcome_mail',
    'user_id' => $user->id,
]);

return [
    'status' => 'success',
];

Worker側では、

while (true) {
    $job = $queue->pop();

    try {
        executeJob($job);
    } catch (Throwable $e) {
        logError($e);

        retryJob($job);
    }
}

といった形で処理します。

実際のシステムでは、リトライ回数や失敗Jobの保存、Worker監視なども追加します。

まとめ

PHPで非同期処理を実装する方法は一つではありません。

複数のHTTP通信を並行して進めたい場合は、curl_multi_exec()が有力な選択肢です。

ユーザーへのレスポンスだけを先に返し、その後に短時間の後続処理を実行したい場合は、PHP-FPM環境でfastcgi_finish_request()を利用できます。

別のプログラムやPHPスクリプトを起動したい場合には、proc_open()などを利用できます。

一方、メール送信、画像処理、CSV生成、動画変換、大量データ処理など、時間のかかる処理をWebリクエストから切り離したい場合は、ジョブキューとWorkerを利用する方法が適しています。

また、ReactPHPやAmpなどのライブラリを利用すれば、大量のネットワークI/Oを効率的に処理するイベント駆動型アプリケーションも構築できます。

Fiberは処理を中断・再開するための仕組みですが、Fiberそのものが自動的に処理を並列化するわけではありません。

PHPで非同期処理を実装する際は、まず「何を非同期化したいのか」を明確にすることが重要です。

複数のAPI通信ならcurl_multi_exec()、レスポンス後の短い処理ならfastcgi_finish_request()、本格的なバックグラウンド処理ならジョブキューというように、目的に応じて使い分けることで、保守性とパフォーマンスの両方を高めやすくなります。

以上、PHPで非同期処理を実装する方法についてでした。

最後までお読みいただき、ありがとうございました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次