探索 PHP 多进程

鉴于上两篇文章,有些同学对php的多进程表现出很大的兴趣,所以我又要来献丑了,感觉不错就点赞,感觉有帮助就打赏,反正要鼓励一下宝宝,哈哈哈。

开始探索

进程是系统进行资源分配和调度的基本单位,执行一个php程序,默认只会生成一个进程,当该进程发生请求IO时,就会被挂起,让出CPU,php的多进程编程的目的就是要在多个进程并发的执行任务,当其中一些进程由于发生IO被挂起时,还有一些进程可以利用CPU执行任务,这样就可以充分利用系统资源来完成我们的大批量任务了(特别适合IO型任务)。php多进程开发,主要有两条路:

  1. 第一就是php内置的pcntl扩展(依赖linux系统),
  2. 第二就是比较流行的扩展swoole下的多进程。

以下我们来实际操作一波!

1. pcntl_fork

不多说了,直接上代码:

        $pid = pcntl_fork(); //fork一个子进程
        if ($pid > 0) { //父进程会获取到fork到的子进程的进程id
            dump('我是父进程');
        } elseif ($pid == 0) { //$pid=0时为刚刚派生出的子进程
            dump('我是子进程');
        } else {
            dump('创建子进程失败');
        }

执行结果如下,证明创建成功:

探索 PHP 多进程

就那么简单吗?不是的。还有很多关键进程创建和使用过程要注意的点,以下再来深入探索一下:

  1. 孤儿进程

孤儿进程已经很白话了,创建它的父进程死掉了,但是它自己还没死,还在运行,但是linux宪法规定,凡是父进程死了还在运行的子进程都要认进程号为1的进程做父亲,这样的话,子进程就是可以脱离ssh,直接进程后台运行模式,由干爹进程1来管理了。利用这一点,可以开发守护进程。

        $pid = pcntl_fork();
        if ($pid > 0) {
            $seconds = 5;
            while ($seconds--) {
                dump('我是父进程,我的寿命只有5秒,剩余:' . $seconds);
                sleep(1);
            }
            dump('我是父进程');
        } elseif ($pid == 0) {
            $seconds = 10;
            while ($seconds--) {
                dump('我是子进程,我的寿命有10秒,剩余::' . $seconds);
                sleep(1);
            }
        } else {
            dump('创建子进程失败');
        }

执行结果如下

探索 PHP 多进程

前5秒,父子进程协同运行,保证正常的父子关系

systemd,1 --switched-root --system --deserialize 22
  ├─sshd,1090 -D -u0
  │   ├─sshd,3711
  │   │   └─sshd,3713
  │   │       └─bash,3714
  │   │           └─sudo,3741 -i
  │   │               └─bash,3742
  │   │                   └─php,11178 think debug  //父进程
  │   │                       └─php,11179 think debug  //子进程

后5秒,父进程死掉了,子进程退出ssh,认了进程号为1的进程做干爹,继续执行5秒,执行完毕被进程1收回,如果一直执行就成为守护进程了。

systemd,1 --switched-root --system --deserialize 22
  ├─php,11179 think debug
  1. 僵尸进程

僵尸进程就是已经执行完了,exit了,但是父进程没有主动回收资源,父子进程关系没有变化,所以依然存在于进程表中。由于僵尸进程会占用系统资源,所以是有危害的,应该避免。怎么模拟产生呢,很简单,将上面的代码变一下,使父进程长命于子进程,如下:

        $pid = pcntl_fork();
        if ($pid > 0) {
            $seconds = 10;
            while ($seconds--) {
                dump('我是父进程,我的寿命只有10秒,剩余:' . $seconds);
                sleep(1);
            }
            dump('我是父进程');
        } elseif ($pid == 0) {
            $seconds = 5;
            while ($seconds--) {
                dump('我是子进程,我的寿命有5秒,剩余::' . $seconds);
                sleep(1);
            }
        } else {
            dump('创建子进程失败');
        }
    }

执行结果如下:

探索 PHP 多进程

前5秒,父子进程协同运行,保证正常的父子关系

systemd,1 --switched-root --system --deserialize 22
  ├─sshd,1090 -D -u0
  │   ├─sshd,3711
  │   │   └─sshd,3713
  │   │       └─bash,3714
  │   │           └─sudo,3741 -i
  │   │               └─bash,3742
  │   │                   └─php,11204 think debug
  │   │                       └─php,11205 think debug

后5秒,子进程死掉了,父进正常运行,但是子进程还存在于进程表中,占用系统资源,直到父进程也死掉才会一起被释放。

  ├─sshd,1090 -D -u0
  │   ├─sshd,3711
  │   │   └─sshd,3713
  │   │       └─bash,3714
  │   │           └─sudo,3741 -i
  │   │               └─bash,3742
  │   │                   └─php,11204 think debug
  │   │                       └─(php,11205)  //僵尸进程
  1. 避免僵尸进程和孤儿进程

想要避免出现僵尸进程和孤儿进程,主要是保证两点:一是,父进程不能早死于子进程;二是,父进程能及时回收运行结束的子进程。这方面,主要使用php提供了两个函数来解决:pcntl_wait、pcntl_waitpid。编辑代码如下:

        $pid = pcntl_fork();
        if ($pid) {
            dump('这里是父进程');
            pcntl_wait($status); //主进程会挂起,等待第一个子进程结束
            //等同于, pcntl_waitpid($pid,$status);
            sleep(10); //模拟干活
        } elseif ($pid == 0) {
            sleep(20); //模拟干活
            dump('这里是子进程');
        } else {
            dump('创建子进程异常');
        }

运行上面代码,发现僵尸进程和孤儿进程问题解决了,因为主进程始终保持等待和回收子进程,但是这样之后,任务执行变成了串行的、同步的,根本实现不了父子进程同时干活的目的。

  1. 多进程异步非阻塞工作

毫不犹豫,怼代码:

        $pids = [];
        for ($i = 0; $i < 20; $i++) {
            $pid = pcntl_fork();
            if ($pid) {
                $pids[] = $pid;
            } elseif ($pid == 0) {
                sleep(20);
                dump('这里是子进程');
                exit();//执行完,一定要结束,不然就会走进创建子进程的死循环
            } else {
                dump('创建子进程异常');
            }
        }
        //等待回收所有子进程
        foreach ($pids as $pid) {
            pcntl_waitpid($pid, $status);
            //第三个参数有两个可能值:WNOHANG-回收子进程时异步不挂起主进程
            //                      WUNTRACED-子进程已经退出并且其状态未报告时返回
        }

执行结果如下,整个过程20秒结束

  ├─sshd,1073 -D -u0
  │   ├─sshd,3133
  │   │   └─sshd,3135
  │   │       └─bash,3136
  │   │           └─sudo,3163 -i
  │   │               └─bash,3164
  │   │                   └─php,3361 think debug
  │   │                       ├─php,3363 think debug
  │   │                       ├─php,3364 think debug
  │   │                       ├─php,3365 think debug
  │   │                       ├─php,3366 think debug
  │   │                       ├─php,3367 think debug
  │   │                       ├─php,3368 think debug
  │   │                       ├─php,3369 think debug
  │   │                       ├─php,3370 think debug
  │   │                       ├─php,3371 think debug
  │   │                       ├─php,3372 think debug
  │   │                       ├─php,3373 think debug
  │   │                       ├─php,3374 think debug
  │   │                       ├─php,3375 think debug
  │   │                       ├─php,3376 think debug
  │   │                       ├─php,3377 think debug
  │   │                       ├─php,3378 think debug
  │   │                       ├─php,3379 think debug
  │   │                       ├─php,3380 think debug
  │   │                       ├─php,3381 think debug
  │   │                       └─php,3382 think debug

这里的代码设计,主进程负责创建、回收子进程,子进程负责实际的干活,整个过程都是异步的,所以执行效率非常高。

  1. 进程间通信-共享内存

php进程间的共享内存函数有两大类,简介如下:

I:首先简单介绍一下shmop_xxx类

相关函数,使用起来也简单,不做一一解释了(偷懒)

探索 PHP 多进程

编辑代码如下

        $key = ftok(__FILE__, 'c'); //ipc标识
        $shmid = shmop_open($key, 'c', 0655, 1024);//申请1024字节的内存空间
        $pid = pcntl_fork();
        if ($pid > 0) {
            sleep(2);//睡眠一下,等待子进程写数据进内存
            $info = shmop_read($shmid, 0, 100);//读取内存数据
            dump('我是父进程,从共享进程中读取到的信息是:' . $info);
            pcntl_wait($status); //回收子进程
        } elseif ($pid == 0) {
            //从申请的共享内存的第0个字节开始写数据
            $size = shmop_write($shmid, "hello father,i'm your son!", 0);
            dump('我是子进程,我向内存里写入字节数是:' . $size);
            exit();
        } else {
            dump('创建子进程失败');
        }

执行结果如下,父子进程能够通过通过共享内存实现信息交流

探索 PHP 多进程

II:再来介绍一下shm_xxx类,需要安装Semaphore 扩展

相关函数,使用起来也简单,不做一一解释了(再次偷懒)

探索 PHP 多进程

编辑代码如下:

       $key = ftok(__FILE__, 'c'); //ipc标识
        $shmid = shm_attach($key, 1024, 0655);
        $variable_key = 1; //类似设置key-value的key,不过这里要求是int
        $pid = pcntl_fork();
        if ($pid > 0) {
            sleep(2);//睡眠一下,等待子进程写数据进内存
            $info = shm_get_var($shmid, $variable_key);//获取对应key的值
            dump('我是父进程,从共享进程中读取到的信息是:' . $info);
            pcntl_wait($status); //回收子进程
        } elseif ($pid == 0) {
            //对key设置值
            $size = shm_put_var($shmid, $variable_key, "hello father,i'm your son!");
            dump('我是子进程,我向内存里写入字节数是:' . $size);
            exit();
        } else {
            dump('创建子进程失败');
        }

        shm_remove($shmid); // 从系统中移除
        shm_detach($shmid); //关闭和共享内存的连接

III:除了上面的方式之外,还可以使用Redis、memcache等方式实现进程间的数据共享

  1. 进程间通信-信号量

信号量相关函数如下,如需细究,请自行研究:

探索 PHP 多进程

以下我怼一下自己粗狂的代码:

        $key = ftok(__FILE__, 'c'); //ipc标识
        $signal_id = sem_get($key); //获取信号id
        $pids = [];
        for ($i = 0; $i < 5; $i++) {
            $pid = pcntl_fork();
            if ($pid > 0) {
                $pids[] = $pid;
                sem_acquire($signal_id); //获取信号量(阻塞式)
                $datetime = date('Y-m-d H:i:s');
                dump("我是父进程,正在创建进程:{$i},创建时间:{$datetime}");
                sem_release($signal_id);//释放信号量
            } elseif ($pid == 0) {
                sem_acquire($signal_id); //获取信号量(阻塞式)
                sleep(2);
                sem_release($signal_id);//释放信号量
                exit();
            } else {
                dump('创建子进程失败');
            }
            sem_remove($signal_id);//删除信号id
            //等待回收所有子进程
            foreach ($pids as $pid) {
                  pcntl_waitpid($pid, $status);
            }
        }

执行结果如下:

探索 PHP 多进程

可以发现,父进程不是一下子生成5个子进程的,因为子进程做了信号量控制,当某个进程获得信号量时,其他进程处于等待信号量时间,只有拥有信号量的进程释放信号量时,其他进程才能获得,所以上面父进程每次创建子进程后,要等子进程睡两秒,然后再继续创建下去,如此类推。

2. swoole多进程

swoole的官网学习文档,https://wiki.swoole.com/wiki/page/p-proces...

总结

进程是十分昂贵的资源,php每个进程占用内存从几M到几百M不等,过多的创建进程反而会严重拖垮系统,使系统变得缓慢,甚至宕机。php进程都是独立的运行单位,进程间的数据库、Redis连接都是独立的,要做好资源使用控制和进程间通信。php多进程编程适用于cli模式,对于php-fpm运行模式下,尽量不要采取多进程编程,会出现无法预测的问题。





原创不易,分享快乐,渴望动力

浅谈并发加锁

本作品采用《CC 协议》,转载必须注明作者和本文链接
我只想看看蓝天
本帖由系统于 4年前 自动加精
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 3

第一段代码和图片我怎么没看懂?

4年前 评论

看到了,不曰了

2年前 评论

如果遇到操作mysql会出现 2006 mysql server has gone away 错误。
解决方法:在子进程处理中加入

if ($pid == 0) {
    //some other process
    posix_kill(getmypid(),SIGKILL);
}
1年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!