pcntl extensionを使って一定個数の子プロセスに作業させる方法

本日はノッキングオンさんで第22回PHP勉強会が開催されたので参加してきました。


その後の宴会で、PHPCLIで使うときにexec()関数ではなくpcntl extensionを使って子プロセスを制御する話をしたら、周りの人の食いつきがいつになく良かったので、このネタでエントリを書くことにします。


まずはコードから。

<?php
// child process params
$nchild = 0;
$nfinished = 0;
$cur_idx = 0;

$maxchild = 10;

$params = range('a', 'z');
$paramlen = count($params);

for (;;) {
    if ($nfinished >= $paramlen) {
        break;
    }
    if (($nchild <= $maxchild) &&
        ($cur_idx < $paramlen)) {
        $pid = pcntl_fork();
        if ($pid == -1) {
            die('fork failed');
        } else if ($pid) {
            // parent process
            ++$nchild;
            ++$cur_idx;
        } else {
            // child process
            $val = $params[$cur_idx];
            debug("- forked ($cur_idx): $val\n");
            $args = array($val);
//            pcntl_exec('/bin/echo', $args);
            sleep(rand(1, 10));
            debug("+ success ($cur_idx)\n");
            exit(0);
        }
    } else {
        $pid = pcntl_waitpid(0, $status, 0);
        --$nchild;
        ++$nfinished;
    }
}

function debug($msg) {
    echo $msg;
}
?>

pcntl extensionはPHPの通常のビルドでは有効にならないので、configure時に'--enable-pcntl'することを忘れないでください。


これをCLIで実行すると、ランダム時間で実行終了する子プロセスが最大10個起動します。一つの子プロセスが終了すると入れ替わりに別の子プロセスが起動しますが、子プロセスの数が10を超えることはありません。すべての$paramsに対して処理を行うとプログラムも終了します。


この例では子プロセスもPHPのコードの続きをそのまま実行していますが、もし別のコマンドを起動したい場合は、コメントアウトされているようにpcntl_exec()関数を用いてそのコマンドを実行します。その場合は子プロセスのpcntl_exec()以降の部分は実行されないので注意してください。


もともとこの手法は、RSSを取得するクローラープログラムを作成していたときに開発したものです。最初は1プロセスで順番にRSSを取得していたのですが、ネットワークの接続待ちなどで引っかかることが多く、また長時間起動しているとメモリリークしているのかどんどんプロセスサイズが大きくなり、それにつれて動作もどんどん遅くなっていたので、子プロセスを起動する方法に切り替えました。


コードを見れば分かるように、やっていることはとてもシンプルですが案外知られていない方法のようだったのでご紹介しました。PHPコマンドラインで使うのも案外楽しいものです。


2011-12-08追記:
クラス化してより汎用性を高めたものを

子プロセス制御ふたたび : PHP Advent Calendar jp 2011 Day 8 - Blog::koyhoge

として公開しました。