YiluPHP
这家伙很懒,什么都没有留下...

经验 实例讲解:学会这两个函数,你就能玩转多进程

浏览数 26020 最后修改时间
​本文通过6个例子,让你彻底搞懂如何fork子进程,和监听子进程是否结束。轻松搞定:让PHP创建多个子进程,高效处理大批量数据。

创建子进程的作用

答:创建多个子进程,可以让多个子进程同时处理大量的数据,提高处理数据的效率,例如:发送10万个红包、给10万个用户发送开课提醒。
<?php

class pcntl
{
    /**
     * 创建子进程的作用
     * 答:创建多个子进程,可以让多个子进程同时处理大量的数据,提高处理数据的效率,例如:发送10万个红包、给10万个用户发送开课提醒
     */
    public function demo_1()
    {
        $data = [[1, 2], [3, 4], [5, 6]];
        foreach ($data as $i => $ids) {
            $pid = pcntl_fork();
            if ($pid == -1) {
                echo "创建子进程失败 \$i = $i \n";
            }
            elseif ($pid) { //pid不为0则说明是父进程
                echo "我是父进程 \$i = $i pid = $pid \n";
            }
            else { //新fork出来的子进程的pid为0
                $ids = json_encode($ids);
                echo "   我是子进程, \$i = $i pid = $pid \$ids = $ids \n";
                //这里可以写你的业务代码,例如:给用户发送红包
                sleep(10);
                exit; //需要停止,要不然会执行后面的代码(包括继续foreach之后的循环),后面的代码是给父进程执行的
            }
        }
        sleep(10);
        echo "父进程结束了 \n";
}

$demo = new pcntl();
$demo->demo_5();

查看进程数有4个,其中1个父进程,3个子进程说明已经成功创建了​子进程。
[root@YiluPHP ~]# ps -ef|grep pcntl
root      3438  2932  0 18:23 pts/1    00:00:00 /usr/local/php74/bin/php pcntl.php
root      3924  3438  0 18:24 pts/1    00:00:00 /usr/local/php74/bin/php pcntl.php
root      3925  3438  0 18:24 pts/1    00:00:00 /usr/local/php74/bin/php pcntl.php
root      3926  3438  0 18:24 pts/1    00:00:00 /usr/local/php74/bin/php pcntl.php
root      4309 27890  0 18:24 pts/0    00:00:00 grep pcntl

执行程序打印结果如下
[root@YiluPHP ~]# /usr/local/php74/bin/php pcntl.php
我是父进程 $i = 0 pid = 3924
   我是子进程, $i = 0 pid = 0 $ids = [1,2]
我是父进程 $i = 1 pid = 3925
   我是子进程, $i = 1 pid = 0 $ids = [3,4]
我是父进程 $i = 2 pid = 3926
   我是子进程, $i = 2 pid = 0 $ids = [5,6]
父进程结束了

如果父进程结束了,子进程会不会跟着强制结束

答案是:子进程不会跟着结束,子进程会继续执行。
<?php

class pcntl
{
    /**
     * 如果父进程结束了,子进程会不会跟着强制结束
     * 答案是:子进程不会跟着结束,子进程会继续执行
     */
    public function demo_2()
    {
        $data = [[1, 2], [3, 4], [5, 6]];
        foreach ($data as $i => $ids) {
            $pid = pcntl_fork();
            if ($pid == -1) {
                echo "创建子进程失败 \$i = $i \n";
            }
            elseif ($pid) { //pid不为0则说明是父进程
                echo "我是父进程 \$i = $i pid = $pid \n";
            }
            else { //新fork出来的子进程的pid为0
                $ids = json_encode($ids);
                echo "   我是子进程, \$i = $i pid = $pid \$ids = $ids \n";
                //这里可以写你的业务代码,例如:给用户发送红包
                sleep(3);
                echo "\n再见,\$i = $i \n";
                exit; //需要停止,要不然会执行后面的代码,后面的代码是给父进程执行的
            }
        }
        sleep(1);
        echo "\n父进程结束了 \n";
}

$demo = new pcntl();
$demo->demo_5();

运行程序打印结果如下,可以看到父进程先结束,子进程按自己所需要的时间结束:
[root@YiluPHP ~]# /usr/local/php74/bin/php pcntl.php
我是父进程 $i = 0 pid = 31430
   我是子进程, $i = 0 pid = 0 $ids = [1,2]
我是父进程 $i = 1 pid = 31431
   我是子进程, $i = 1 pid = 0 $ids = [3,4]
我是父进程 $i = 2 pid = 31432
父进程结束了
   我是子进程, $i = 2 pid = 0 $ids = [5,6]
再见,$i = 0
再见,$i = 1
再见,$i = 2

子进程能不能改变父进程的值,进而改变其它子进程的值

答案是:不能
创建子进程时会拷贝一份当时已经存在的所有变量,之后各个子进程之间就像不同的http请求一样,变量、类的静态变量都是互不影响的。
<?php

class pcntl
{
    /**
     * 子进程能不能改变父进程的值,进而改变其它子进程的值
     * 答案是:不能
     * 创建子进程时会拷贝一份当时已经存在的所有变量,之后各个子进程之间就像不同的http请求一样,变量、类的静态变量都是互不影响的
     */
    public function demo_3()
    {
        $data = [[1, 2], [3, 4], [5, 6]];
        $count = -1;
        foreach ($data as $i => $ids) {
            $pid = pcntl_fork();
            if ($pid == -1) {
                echo "创建子进程失败 \$i = $i \n";
            }
            elseif ($pid) { //pid不为0则说明是父进程
                echo "我是父进程 \$i = $i pid = $pid \n";
            }
            else { //新fork出来的子进程的pid为0
                $ids = json_encode($ids);
                echo "   我是子进程, \$i = $i pid = $pid \$ids = $ids \n";
                if ($i == 0) { //在第1个子进程中把 $count 的值设置为666
                    $count = 666;
                }
                //这里可以写你的业务代码,例如:给用户发送红包
                sleep($i+1);
                echo "\n再见,\$i = $i \$count = $count \n";
                exit; //需要停止,要不然会执行后面的代码,后面的代码是给父进程执行的
            }
        }
        sleep(6);
        echo "\n父进程结束了 \$count = $count \n";
}

$demo = new pcntl();
$demo->demo_5();

运行程序打印结果如下,可以看出第1个子进程只改变了自己的 $count 的值,没有改变父进程的,也没改变其它子进程的:
[root@YiluPHP ~]# /usr/local/php74/bin/php pcntl.php
我是父进程 $i = 0 pid = 7875
   我是子进程, $i = 0 pid = 0 $ids = [1,2]
我是父进程 $i = 1 pid = 7876
   我是子进程, $i = 1 pid = 0 $ids = [3,4]
我是父进程 $i = 2 pid = 7877
   我是子进程, $i = 2 pid = 0 $ids = [5,6]
再见,$i = 0 $count = 666
再见,$i = 2 $count = -1
再见,$i = 1 $count = -1
父进程结束了 $count = -1

父进程如何知道所有的子进程都已经结束

答:使用 pcntl_waitpid 函数监听子进程的状态。
<?php

class pcntl
{
    /**
     * 父进程如何知道所有的子进程都已经结束
     * 答:使用 pcntl_waitpid 函数监听子进程的状态
     */
    public function demo_4()
    {
        $data = [[1, 2], [3, 4], [5, 6]];
        $count = 0;
        foreach ($data as $i => $ids) {
            $count++; //加一个子进程数,最终肯定会将值加到4
            $pid = pcntl_fork();
            if ($pid == -1) {
                echo "创建子进程失败 \$i = $i \n";
            }
            elseif ($pid) { //pid不为0则说明是父进程
                echo "我是父进程 \$i = $i pid = $pid \n";
            }
            else { //新fork出来的子进程的pid为0
                $ids = json_encode($ids);
                echo "   我是子进程, \$i = $i pid = $pid \$ids = $ids \n";
                sleep(rand(1, 3));
                echo "\n再见,\$i = $i\n";
                exit; //需要停止,要不然会执行后面的代码,后面的代码是给父进程执行的
            }
        }

        //此处监听子进程的状态
        while (($pid2 = pcntl_waitpid(-1, $status, WUNTRACED)) > 0) { //返回已经结束的子进程的pid
            echo "\n子进程 $pid2 已结束 \n";
            $count--;
        }

        echo "\n父进程结束了 \$count = $count \n";
}

$demo = new pcntl();
$demo->demo_5();

通过打印可以看到,只有所有的子进程结束之后,父进程才会结束,可以推断出:在整个过程中, $count的值加到4,然后再减到0。
[root@YiluPHP1 ~]# /usr/local/php74/bin/php pcntl.php
我是父进程 $i = 0 pid = 17940
   我是子进程, $i = 0 pid = 0 $ids = [1,2]
我是父进程 $i = 1 pid = 17941
   我是子进程, $i = 1 pid = 0 $ids = [3,4]
我是父进程 $i = 2 pid = 17942
   我是子进程, $i = 2 pid = 0 $ids = [5,6]
再见,$i = 1
再见,$i = 0
再见,$i = 2
子进程 17941 已结束
子进程 17940 已结束
子进程 17942 已结束
父进程结束了 $count = 0

当子进程的程序因致命错误中断了,使用 pcntl_waitpid 函数能监听到子进程已结束吗?

答案是:可以监听到。
<?php

class pcntl
{
    /**
     * 当子进程的程序因致命错误中断了,使用 pcntl_waitpid 函数能监听到子进程已结束吗
     * 答案是:可以监听到
     */
    public function demo_5()
    {
        $data = [[1, 2], [3, 4], [5, 6]];
        $count = 0;
        foreach ($data as $i => $ids) {
            $count++; //加一个子进程数,最终肯定会将值加到4
            $pid = pcntl_fork();
            if ($pid == -1) {
                echo "创建子进程失败 \$i = $i \n";
            }
            elseif ($pid) { //pid不为0则说明是父进程
                echo "我是父进程 \$i = $i pid = $pid \n";
            }
            else { //新fork出来的子进程的pid为0
                $ids = json_encode($ids);
                echo "   我是子进程, \$i = $i pid = $pid \$ids = $ids \n";
                if ($i == 2) { //只让第3个子进程意外中断
                    no_fun(); //调用一个不存在的函数,制造一个致命错误让程序中断
                }
                sleep(3);
                echo "\n再见,\$i = $i\n";
                exit; //需要停止,要不然会执行后面的代码,后面的代码是给父进程执行的
            }
        }

        while (($pid2 = pcntl_waitpid(-1, $status, WUNTRACED)) > 0) { //返回已经结束的子进程的pid
            echo "\n子进程 $pid2 已结束 \n";
            $count--;
        }

        echo "\n父进程结束了 \$count = $count \n";
}

$demo = new pcntl();
$demo->demo_5();

通过打印可以看到,第3个子进程提前结束了,它没有输出“再见”的字符,但是父进程也正常的结束了,$count 最终减到了0。
[root@YiluPHP1 ~]# /usr/local/php74/bin/php pcntl.php
我是父进程 $i = 0 pid = 17229
   我是子进程, $i = 0 pid = 0 $ids = [1,2]
我是父进程 $i = 1 pid = 17230
   我是子进程, $i = 1 pid = 0 $ids = [3,4]
我是父进程 $i = 2 pid = 17231
   我是子进程, $i = 2 pid = 0 $ids = [5,6]
Fatal error: Uncaught Error: Call to undefined function no_fun() in pcntl.php on line 215
Error: Call to undefined function no_fun() in pcntl.php on line 215
Call Stack:
    0.0022     350952   1. {main}() pcntl.php:0
    0.0023     350992   2. pcntl->demo_5() pcntl.php:305
子进程 17231 已结束
再见,$i = 0
再见,$i = 1
子进程 17230 已结束
子进程 17229 已结束
父进程结束了 $count = 0

如果手工杀掉子进程,使用 pcntl_waitpid 函数能监听到子进程已结束吗?

答案是:可以监听到。
<?php

class pcntl
{
    /**
     * 如果手工杀掉子进程,使用 pcntl_waitpid 函数能监听到子进程已结束吗
     * 答案是:可以监听到
     */
    public function demo_6()
    {
        $data = [[1, 2], [3, 4], [5, 6]];
        $count = 0;
        foreach ($data as $i => $ids) {
            $count++; //加一个子进程数,最终肯定会将值加到4
            $pid = pcntl_fork();
            if ($pid == -1) {
                echo "创建子进程失败 \$i = $i \n";
            }
            elseif ($pid) { //pid不为0则说明是父进程
                echo "我是父进程 \$i = $i pid = $pid \n";
            }
            else { //新fork出来的子进程的pid为0
                $ids = json_encode($ids);
                echo "   我是子进程, \$i = $i pid = $pid \$ids = $ids \n";
                sleep(30);
                echo "\n再见,\$i = $i\n";
                exit; //需要停止,要不然会执行后面的代码,后面的代码是给父进程执行的
            }
        }

        while (($pid2 = pcntl_waitpid(-1, $status, WUNTRACED)) > 0) { //返回已经结束的子进程的pid
            echo "\n子进程 $pid2 已结束 \n";
            $count--;
        }

        echo "\n父进程结束了 \$count = $count \n";
}

$demo = new pcntl();
$demo->demo_5();

先查看正在运行的进程数,有4个进程。
[root@YiluPHP1 ~]# ps -ef|grep pcntl
root     29784  2932  0 19:31 pts/1    00:00:00 /usr/local/php74/bin/php pcntl.php
root     30258 29784  0 19:31 pts/1    00:00:00 /usr/local/php74/bin/php pcntl.php
root     30259 29784  0 19:31 pts/1    00:00:00 /usr/local/php74/bin/php pcntl.php
root     30260 29784  0 19:31 pts/1    00:00:00 /usr/local/php74/bin/php pcntl.php
root     30463 27890  0 19:31 pts/0    00:00:00 grep pcntl

结束掉pid为30259的子进程。
[root@YiluPHP1 ~]# kill 30259

再次查看进程数,确定子进程已经被杀死了,只剩下3个进程。
[root@YiluPHP1 ~]# ps -ef|grep pcntl
root     29784  2932  0 19:31 pts/1    00:00:00 /usr/local/php74/bin/php pcntl.php
root     30258 29784  0 19:31 pts/1    00:00:00 /usr/local/php74/bin/php pcntl.php
root     30260 29784  0 19:31 pts/1    00:00:00 /usr/local/php74/bin/php pcntl.php
root     31411 27890  0 19:32 pts/0    00:00:00 grep pcntl

通过打印结果可以看到,父进程能捕捉到手工杀掉的子进程状态,父进程可以正常结束:
我是父进程 $i = 0 pid = 30258
   我是子进程, $i = 0 pid = 0 $ids = [1,2]
我是父进程 $i = 1 pid = 30259
   我是子进程, $i = 1 pid = 0 $ids = [3,4]
我是父进程 $i = 2 pid = 30260
   我是子进程, $i = 2 pid = 0 $ids = [5,6]
子进程 30259 已结束
再见,$i = 0
再见,$i = 2
子进程 30258 已结束
子进程 30260 已结束
父进程结束了 $count = 0

最后大家想一个问题:这里生成了3个子进程,如果其中1个子进程结束了,如何补加1个子进程呢?这样就可以始终保持有3个子进程在运行。

我来说说