PHPでループを並列処理する

# ループの並列実行

配列をループで処理すると、一つずつ順番に実行されます。非常にCPU時間を使う処理だったり、ネットワークアクセスなどがあり待ち時間が長い場合、一つ一つの実行に時間がかかり効率的に処理することが出来ません。

他の言語だとスレッドなどを使って並列処理をするのですが、PHPにはスレッドありません。ただ、forkが使えるので、これを使って並列処理をすることが出来ます。

以前に、別のプロジェクトでこの方法を使用したところ、大幅な処理時間削減をすることが出来ました。もう少しお手軽に使えるものがあったらよいと思い、汎用的なクラスとして実装してみました。名前はParallelForとしてみました。あと、珍しくドキュメントとかテストも書いてみました。

https://github.com/hironobu-s/parallel-for

ちなみに.NET Frameworkには同名のメソッドがあるそうです、あとで知りました・・・。


# 仕組み

README.mdより引用します

処理対象の配列をより小さな複数個の配列に分割して、それぞれをpcntl_forkで作成した子プロセスで処理し、結果をマージして返します。分割数は設定で変更できます。

配列の要素に対する処理内容と結果をマージする処理は、それぞれクロージャを作成して渡します。

詳しくはexampleディレクトリとtestディレクトリ内のファイルを見てください。


# 実際の例

sleepして引数に文字列を追加するだけの関数longtime()を用意しました。実行には500msかかります。これを時間のかかる処理を行う関数の代わりとして使用します。

この関数を、長さ10の配列の要素全てに適用するとします。単純に実装すると、以下のようなコードになります。

```php

function longtime ($item) {
    // 擬似的に500msをシミュレートしています
    usleep(500000);

    // 引数に "processed." を追加して返す
    return $item . " processed.";
};

$data = array();
for($i = 0; $i < 10; $i++) {
    $data[] = "test$i";
}

$result = array();
foreach($data as $item) {
  $result[] = longtime($item);
}

var_dump($result);

```

結果は以下のようになります

```bash
# time php longtime.php
array(10) {
  [0]=>
  string(22) "test0 processed."
  [1]=>
  string(22) "test1 processed."
  [2]=>
  string(22) "test2 processed."
  [3]=>
  string(22) "test3 processed."
  [4]=>
  string(22) "test4 processed."
  [5]=>
  string(22) "test5 processed."
  [6]=>
  string(22) "test6 processed."
  [7]=>
  string(22) "test7 processed."
  [8]=>
  string(22) "test8 processed."
  [9]=>
  string(22) "test9 processed."
}
php longtime.php  0.07s user 0.01s system 1% cpu 5.095 total
```

私の環境で実行すると5.095秒かかりました。500ms * 10 なので順当な結果です。

次に、ParallelForを使用して、同じ実装を行ってみます。

```php
require_once 'autoload.php';

function longtime($item) {
    // 擬似的に500msをシミュレートしています
    usleep(500000);

    // 引数に "processed." を追加して返す
    return $item . " processed.";
};


$executor = function($data) {
    $results = array();
    foreach($data as $item) {
        $results[] = longtime($item);
    }
    return $results;
};

$data = array();
for($i = 0; $i < 10; $i++) {
    $data[] = "test$i";
}

$result = array();

$p = new ParallelFor;
$p->setNumChilds(4);
$result = $p->run($data, $executor);
var_dump($result);
```

```bash
# time php longtime.php
array(10) {
  [0]=>
  string(22) "test9 processed."
  [1]=>
  string(22) "test0 processed."
  [2]=>
  string(22) "test1 processed."
  [3]=>
  string(22) "test2 processed."
  [4]=>
  string(22) "test6 processed."
  [5]=>
  string(22) "test7 processed."
  [6]=>
  string(22) "test8 processed."
  [7]=>
  string(22) "test3 processed."
  [8]=>
  string(22) "test4 processed."
  [9]=>
  string(22) "test5 processed."
}
php longtime.php  0.09s user 0.03s system 7% cpu 1.593 total
```

1.59秒で完了、半分以下の時間で終わりました。

処理結果の順番が変わっていますが、これはParallelForが元の配列を分割して並列に処理するためです。並列に実行した結果が順不同で完了するため、結果の順番が元の配列の順番とは変わってくるのです。

----

実装内容を簡単に紹介します。

longtime関数は全く同じです。

executorというクロージャがありますが、これは子プロセスの処理内容になります。クロージャの中身は、前のサンプルのforeachの部分と全く同じです。処理内容が同じなので当然です。ただ、ParallelForが元の配列を分割し、分割したうちの一つがdataに渡されてくるので、$dataの要素数は10ではありません。今回は並列数を4にしたので、要素数は3か1になります。

途中にsetNumChilds(4)と言うメソッド呼び出しがあります。これは並列数を4に設定するという意味です。並列数をどのくらいにするかは、処理内容や実行環境のCPUコア数によって最適な値が変わると思います。