PHP多进程

PHP多进程

1.多开几个进程,这种方式简单实用,推荐,比如说使用shell脚本:

#!/bin/bash
 
for((i=1;i<=8;i++))
do    
    /usr/bin/php multiprocessTest.php &
done
 
wait

2.pcntl扩展

php多进程需要pcntl,posix扩展支持,可以通过 php -m 查看,而且多进程实现只能在cli模式下,虽然是个残废,不妨也了解一下, 实际上这些都是调用了Linux的系统API
举个例子:

这个例子非常简单,循环创建5个进程,在各个进程里面打印一句话,主要使用的方法就是函数 pcntl_fork,一次调用两次返回,在父进程中返回子进程pid,在子进程中返回0,出错返回-1。

执行结果如下:

fork the 1th child, pid: 7326
I am the parent, pid: 7326
fork the 2th child, pid: 7327
I am the parent, pid: 7327
fork the 3th child, pid: 7328
I am the parent, pid: 7328
fork the 4th child, pid: 7329
I am the parent, pid: 7329
fork the 5th child, pid: 7330
I am the parent, pid: 7330

先解释一下为什么会产生10条打印结果,第一条结果是子进程打印的,第二条是在父进程打印的!

第一个坑:

如果是在循环中创建子进程,那么子进程中最后要exit,防止子进程进入循环!

第二个坑:

必须等待子进程执行完任务, 有一个简单方法是使用 pcntl_wait,如果不加这个你会发现一个是执行的顺序不固定,第二个就是创建的进程会少于5个,但是加了你会发现这个完全变成并行了...上面的结果就是

然后找了找,发现下面这种写法:

 $pid) {
    if ($pid) {
        pcntl_waitpid($pid, $status);
    }
}

结果如下:

fork the 1th child, pid: 8392
I am the parent, pid: 8392
I am the parent, pid: 8393
fork the 2th child, pid: 8393
I am the parent, pid: 8394
I am the parent, pid: 8395
I am the parent, pid: 8396
fork the 3th child, pid: 8394
fork the 4th child, pid: 8395
fork the 5th child, pid: 8396

找了一张图,大体解释了总体流程:

image

说简单其实也挺简单,几行代码就可以写出一个多进程程序,实现并行编程,但是这里其实还有不少坑,比如僵尸进程,孤儿进程, 守护进程,具体的我也不太熟悉不多讲,再看一个关于进程信号的东西,有些项目里面有时候会用到一些脚本,比如处理redis队列的脚本,通常的做法是写一个while死循环一直从redis里面取数据处理,为了防止内存泄露或者假死,一般都会定时的杀掉脚本重启脚本,但是杀的不好可能会导致数据丢失,举个例子,假如你这个脚本刚好从redis取了一条数据正在处理中,操作还未完成,你突然终止进程,那这个数据就丢失了。至于说服务器挂掉这种情况毕竟不多见,真要解决这种问题还得从队列上入手。

image

Linux进程信号分为很多种,kill -l 可以查看,PHP里面定义了43种,咱就说说常用的几种:

SIGINT 2 这个其实相对于 ctrl+c

SIGTERM 15 就是 kill 默认的参数,表示终止信号,但是你发了信号程序不一定响应

SIGKILL 9 就是 kill -9, 表示立马终止,这个信号在PHP里面是无法注册的,所以一定能成功

看明白了这个就可以读懂上面的例子了,其中 pcntl_signal 是注册信号处理handler,第一个参数是你需要注册的信号,第二个是处理操作,可以是匿名函数或者一个函数名,可以注册多个信号。pcntl_signal_dispatch 调用每个等待信号通过pcntl_signal() 安装的处理器。早期PHP还有一种写法是使用 ticks,性能非常差,php5.3之后建议都使用 pcntl_signal_dispatch。

说明一下:pcntl_signal()函数仅仅是注册信号和它的处理方法,真正接收到信号并调用其处理方法的是pcntl_signal_dispatch()函数必须在循环里调用,为了检测是否有新的信号等待dispatching。

上面的例子执行结果就是当你使用 ctrl+c 的话是无法终止程序的,只有使用 kill pid 这种形式才可以,但是并不是立马就退出,它是代码执行到循环顶部 pcntl_signal_dispatch 地方的时候才会退出,这就保证了你使用kill杀掉进程的时候并不会丢失数据,说好听点这也算是平滑重启吧!

由于进程的系统开销比较大,一般不太适合拿来做大规模并发程序,拿来写个3-5个进程的后台脚本倒是有点用,下面就是我写的一个用来爬取xhprof的数据的脚本,使用了3个进程同时爬取实战,路径,免费课的日志然后做统计根据出现次数排序!

 '10.100.133.99',
    'sz'  => '10.100.135.23',
    'lj'  => '10.100.17.13',
];

$ids = [];

foreach ($servers as $key => $server) {
    $ids[] = $pid = pcntl_fork();
    if ($pid === -1) {
        echo "failed to fork!\n";
        exit;
    } elseif ($pid) {
    } else {
        download($server, $key);
    }
}

foreach ($ids as $i => $pid) {
    if ($pid) {
        pcntl_waitpid($pid, $status);
    }
}

function download($server, $fileName)
{
    $saveDir = SAVE_DIR;
    if (!is_dir(SAVE_DIR)) {
        $saveDir = __DIR__;
    }

    $file = $saveDir . "/xhprof_{$fileName}_tmp.txt";
    $fp   = fopen($file, 'w+');
    foreach (range(1, TOTAL_PAGE) as $page) {
        print_r("### " . date('Y-m-d H:i:s') . ": 正在爬取 $server -> $fileName 第 $page 页...\n");
        try {
            $html = file_get_contents("http://{$server}/xhprof/index.php?page={$page}&ms=" . MS . "&day=" . DAY);
        } catch (Exception $exception) {
            var_dump("网络请求失败!\n");
            exit;
        }
        if (!$html) {
            var_dump("网络请求失败!\n");
            exit;
        }

        preg_match_all("/(.*)<\\/a>/", $html, $matches);
        if (isset($matches[1])) {
            if (count($matches[1]) <= 3) {
                break;
            }
            foreach ($matches[1] as $match) {
                fwrite($fp, $match . "\n");
            }
        }
    }
    fclose($fp);
    print_r("### " . date('Y-m-d H:i:s') . ": 爬取完成,开始处理数据...\n");
    print_r("---------------------------------------------------------- \n");
    $fp = file($file);
    if (!$fp) {
        var_dump("文件读取失败!\n");
    }

    foreach ($fp as $key => $item) {
        $item = rtrim(parse_url(trim($item))['path'], "/");
        if (substr($item, 0, 1) != '/') {
            unset($fp[$key]);
            continue;
        }
        $fp[$key] = preg_replace("/\/\d+/", "/*", $item);
    }

    $res = array_count_values($fp);

    uasort($res, function ($a, $b) {
        return $a < $b;
    });
    $saveFile = fopen($saveDir . "/xhprof_{$fileName}.txt", 'w+');

    foreach ($res as $key => $value) {
        $key = trim($key);
        $str = sprintf("%-50s ===============> %s 次\n", $key, $value);
        fwrite($saveFile, $str);
    }

    fclose($saveFile);
    unlink($file);
    exit;
}

最后还忘记说了一个坑,在子进程里面使用mysql 或者 redis 这类程序有个bug,假如你使用的是单例模式的话,这个连接被多个子进程使用就会出问题,所以如果要使用,必须在各个子进程内部新建一个连接!

你可能感兴趣的:(PHP多进程)