自社サービスのインタラクションデザインを改善するためにPHPで非同期処理をしてみた

はじめまして、こんにちは。
[今年の10月に福岡で起業して、なんちゃってCTOをしている赤と黒が好きな若造です。](https://twitter.com/9th_tech_Ryo)
今回は開発中の自社サービスのインタラクションデザインを改善するためにPHPで非同期処理をしてみたという話を書きます。

# PHPで処理を並行したい。
ユーザーがあるアクションを起こした時に、PHPでの処理に10秒掛かるとします。
その間ユーザーは10秒待つ羽目になるわけですが、この時間が貴重な現代社会に置いて、ユーザーは10秒待つと当然イライラします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/920765/a3e037f5-b75c-a43f-74ee-0dbcbc6803e8.png)
なのでなるべく早くリクエストからレスポンスの時間を短くしたいわけですが、いくらPHP8で爆速になったとは言え、限界もあります。
そこで「重要な処理」と「重要では無い処理」で処理自体を分割して
「重要な処理」が終わった時点でレスポンスを返し、「重要では無い処理」は裏で並列で処理するアイディアを思いつきました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/920765/9d958b1b-c9b2-2c57-c712-757db187fff6.png)
これなら実質3秒でユーザーにレスポンスを返す事ができます！


## PHPはシングルスレッドだよ
ですが残念なことにPHPは基本的にシングルスレッドで、並列処理は出来ません。[^1]
**だから無理です。**
今日のアドベントカレンダー記事は以上です。あざした。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/920765/b273ca8f-ab9e-e05b-4a18-100c0241c624.png)
　


　


…というわけにはいきません。
PHPはシングルスレッドですが、マルチプロセスは可能です。
「並列処理」ではなくて「分割処理」という言葉のほうがイメージしやすいですね	
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/920765/49f646fe-af29-6615-5042-6abc618b926d.png)
シングルスレッドとマルチプロセスは結果は同じ様に見えますが、プロセスが分割されているので、変数などの情報を共有できないのは注意しなくてはいけません。
（正確にはメモリが共有出来ない）

ではどうやってプロセスを分けるのか、を解説していきます。

## exec関数
詳しい解説はPHPの公式のマニュアルに任せます。
PHPマニュアル：[https://www.php.net/manual/ja/function.exec.php](https://www.php.net/manual/ja/function.exec.php)

**PHP 7.4.10**の環境で以下のコードを書いてみました。
exec関数で「返り値を/dev/nullに捨てて、バックグラウンド処理する、phpのプロセス」を立ち上げるコマンドを叩くコードです。

```php:index.php
<?php
    echo "START";
    error_log("\n\n".date('Y-m-d H:i:s')."  START", 3, __DIR__.'/log.txt');

    exec('php '.__DIR__.'/num1_wait5.php > /dev/null &');
    exec('php '.__DIR__.'/num2_wait1.php > /dev/null &');
    exec('php '.__DIR__.'/num3_wait10.php > /dev/null &');
    exec('php '.__DIR__.'/num4_wait0.php > /dev/null &');

    error_log("\n".date('Y-m-d H:i:s')."  END", 3, __DIR__.'/log.txt');
    echo "END";
```

```num1_wait5.php
<?php
    sleep(5);
    error_log("\n".date('Y-m-d H:i:s')."  NUM1_WAIT5", 3, __DIR__.'/log.txt');
```

```num2_wait1.php
<?php
    sleep(1);
    error_log("\n".date('Y-m-d H:i:s')."  NUM2_WAIT1", 3, __DIR__.'/log.txt');
```

```num3_wait10.php
<?php
    sleep(10);
    error_log("\n".date('Y-m-d H:i:s')."  NUM3_WAIT10", 3, __DIR__.'/log.txt');
```

```num4_wait0.php
<?php
    error_log("\n".date('Y-m-d H:i:s')."  NUM4_WAIT0", 3, __DIR__.'/log.txt');
```

index.phpで4つのプロセスを立ち上げて処理するコードです。
実行結果は以下になります。

```
2020-12-12 10:28:00  START
2020-12-12 10:28:00  END
2020-12-12 10:28:00  NUM4_WAIT0
2020-12-12 10:28:01  NUM2_WAIT1
2020-12-12 10:28:05  NUM1_WAIT5
2020-12-12 10:28:10  NUM3_WAIT10
```

図にするとこんな感じです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/920765/07931623-aa2a-e2fc-5327-b6d95200d870.png)

本来であれば全ての処理をシングルプロセスで処理すると完了に```5+1+10+0=16秒```掛かるところを
マルチプロセスにしたおかげで、全ての処理の完了に```10秒```で済んでしまいました。

しかしexec関数はlinuxのコマンドを叩く処理で、マルチプロセスのためのものではありません。
linuxのコマンドを叩くという性質上、うっかりユーザーの入力をそのままexec関数の引数に当てるなんてことは絶対にしないようにしましょう。

##exec関数でのマルチプロセスの欠点
一見簡単にマルチプロセスで処理できるexec関数ですが、実は弱点があります。
処理を分割するためにプロセスを作るという性質上、処理が立て込んだ時には処理が横に広がりすぎて、サーバーに影響を及ぼすからです。
最悪サーバーが落ちると思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/920765/0b2dbd98-3f87-78cf-ffdc-8055c08befa2.png)


#Laravelでの非同期処理
素のPHPでマルチプロセスをしてみましたが、実際の業務やサービスではフレームワークを使うことでしょう。
[株式会社ナインステクノロジーズ](https://ninth.co.jp/)はLaravelが得意なので、Laravelでも非同期処理をやってみましょう。

Laravelではexec関数でのマルチプロセスとは少し違って「ジョブとキュー」で並列処理を実現します。


##ジョブとキュー
厳密に言えば「ジョブとキュー」もマルチプロセス処理の一種で
「ジョブ」と言われる処理の塊を、「キュー」と呼ばれるプロセスが変わって処理するイメージです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/920765/8c561e42-b720-5843-4774-5ad5a8a946d4.png)
最初ジョブは全く無く、キューもジョブが無いので待機状態になっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/920765/9740ccbc-a6e4-43b0-2452-663465fb59fd.png)
なんらかのアクションでジョブが記録されると、キューはそれに気づきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/920765/9375d731-1940-999a-92cb-d29824a5956f.png)
キューは記録されているジョブを1つ取り出して処理を始めます。

こういう流れで処理を非同期的に処理します。

##php artisan queue
詳しい解説はLaravelのドキュメントを翻訳しているサイトに任せます。
Laravel 6.x キュー：[https://readouble.com/laravel/6.x/ja/queues.html](https://readouble.com/laravel/6.x/ja/queues.html)

**Laravel Framework 6.20.6**の環境でジョブとキューの設定をやっていきます。
まずジョブをデータベースに記録したいので、migrationを行います。

```shell
php artisan queue:table
php artisan queue:failed-table    #初期インストール状態では既にmigrationファイルは存在しているのでエラーが出る。
php artisan migrate
```
これで```jobsテーブル```と```failed_jobsテーブル```が作られます。

そしてjobファイルを生成して、書いていきます。

```shell
php artisan make:job Num1Wait5.php
php artisan make:job Num2Wait1.php
php artisan make:job Num3Wait10.php
php artisan make:job Num4Wait0.php
```

```php:/app/Jobs/Num1Wait5.php
<?php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class Num1Wait5 implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    /**
     * Create a new job instance.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle()
    {
        sleep(5);
        error_log("\n".date('Y-m-d H:i:s')."  NUM1_WAIT5", 3, '/log.txt');
    }
}
```

```php:/app/Jobs/Num2Wait1.php
<?php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class Num2Wait1 implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    /**
     * Create a new job instance.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle()
    {
        sleep(1);
        error_log("\n".date('Y-m-d H:i:s')."  NUM2_WAIT1", 3, '/log.txt');
    }
}
```

```php:/app/Jobs/Num3Wait10.php
<?php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class Num3Wait10 implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    /**
     * Create a new job instance.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle()
    {
        sleep(10);
        error_log("\n".date('Y-m-d H:i:s')."  NUM3_WAIT10", 3, '/log.txt');
    }
}
```

```php:/app/Jobs/Num4Wait0.php
<?php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class Num4Wait0 implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    /**
     * Create a new job instance.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle()
    {
        sleep(0);
        error_log("\n".date('Y-m-d H:i:s')."  NUM4_WAIT0", 3, '/log.txt');
    }
}
```

そしてジョブを入れるcontrollerファイルを生成して、書いていきます。

```shell
php artisan make:controller TestController
```

```php:/app/Http/Controllers/TestController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class TestController extends Controller
{
    public function test(){
        echo "START";
        error_log("\n\n".date('Y-m-d H:i:s')."  START", 3, '/log.txt');

        $this->dispatch(new \App\Jobs\Num1Wait5() );
        $this->dispatch(new \App\Jobs\Num2Wait1() );
        $this->dispatch(new \App\Jobs\Num3Wait10() );
        $this->dispatch(new \App\Jobs\Num4Wait0() );

        error_log("\n".date('Y-m-d H:i:s')."  END", 3, '/log.txt');
        echo "END";
    }
}
```

そして```.env```の```QUEUE_CONNECTION```を編集します。


```text:.env
QUEUE_CONNECTION=database
```

最後にキューのプロセスを立ち上げます。

```shell
php artisan queue:work > /dev/null &
```

これで準備完了です。
実際に```TestController@test```を実行した結果が以下です。

```
2020-12-12 12:58:00  START
2020-12-12 12:58:00  END
2020-12-12 12:58:05  NUM1_WAIT5
2020-12-12 12:58:06  NUM2_WAIT1
2020-12-12 12:58:16  NUM3_WAIT10
2020-12-12 12:58:16  NUM4_WAIT0
```

…あれ？

##php artisan queueでのジョブとキューでの欠点

先程の実行結果は、16秒掛かってしまいました。
exec関数では10秒だったのになぜでしょうか？

実は先程の実行は**「キューが1個だったから」**16秒掛かったのです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/920765/0d5d57ae-b957-b766-f5f2-750e47437ae7.png)
キューが1個なので以下の流れで処理をします。

ジョブ１を処理して、
ジョブ１が終わったらジョブ２を処理して、
ジョブ２が終わったらジョブ３を処理して、
ジョブ３が終わったらジョブ４を処理して、
ジョブ４が終わる。

キューは1個なので、処理できるジョブも当然1個です。だから並列的に処理が出来ないんですね。
なので、並列的に処理したい場合はキューの数を増やすと良いでしょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/920765/328cee4a-2f1a-1801-24b1-e992a05b9818.png)
キューを増やすことで同時に処理できるジョブの数が増え、効率的に処理をこなすことが出来ます。

キューの数だけジョブを捌けるのですが、ここにも落とし穴があって
例えばジョブの増加速度よりもキューがジョブを捌く速度が遅くなってしまうと
いつまで立っても処理が開始されないジョブが出てきてしまう可能性があります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/920765/10d2e8b4-4585-05c1-066e-f42deebafddb.png)

まさに炎上状態。
一応Laravelではジョブの優先度やキューへの振り分けを操作出来ますが、万が一こういう自体が起こらない訳ではありません。


#まとめ
PHPはシングルスレッドで並列処理は難しいですが
exec関数やLaravelのqueueで非同期的に処理を実行できます。

活用することでユーザーへのレスポンスが早くなることでしょう。
本当はこの処理と合わせてプログレスバーを実装する予定でしたが、以外と長くなったので今度にします。

もし間違っているところがあればマサカリお願いします。（言葉尻を捉えてイジメるのはやめてくださぃ。


明日は奇しくも同じ福岡の企業に所属するせいけしろーさんです。

ココまで読んでくださってありがとうございました。
LGTMと[Twitter](https://twitter.com/9th_tech_Ryo)をフォローしてくれると嬉しいです！よろしくおねがいします！！！
[@9th_tech_Ryo](https://twitter.com/9th_tech_Ryo)

あと別のアドベントカレンダーになりますが、弊社長も記事を明日書くので、ぜひ見てみてください！！！！！
[https://qiita.com/akira_9th](https://qiita.com/akira_9th)



[^1]: [ptreads](https://www.php.net/manual/ja/book.pthreads.php)というモジュールを使えば可能らしい