如何使用PHP多进程开发

1.使用多进程的一些场景

  重复且耗时的一些操作,例如 发邮件,处理文件,或者是某些批量处理独立个体的事情。例如博主本次用到的场景是批量同步实体信息的操作,每个账户的实体是独立的,量级比较大,且处理逻辑有较多的网络通讯消耗和数据库查询。导致脚本执行经常卡主。卡点主要是处理慢,且并发,机器负载高,导致进程能分到的时间片也不多。

  PHP 的多进程和 协程 并不是一样的概念,一个是基于进程,另一个是基于线程的。我们知道操作系统最小的调度单位是进程,一个进程可以包含至少一个线程。进程间是相互隔离的,进程间的通讯只能基于一些特殊的方式,如管道、共享内存、消息队列等方式,而线程是共享进程资源的,所以通讯不需要那么麻烦,但是线程安全问题却又是一个弊端,资源是共享的,导致大家都有权限去动它,就会容易出问题,所以互斥锁是必要的,导致多线程开发会比多进程开发会复杂一些,后面我写到关于swoole的一些文章时我们再进行讨论。

2.php 如何 使用多进程

我们知道,c 语言 是基于 fork 函数进行子进程的创建的。Linux 也是基于这个原理,所有的Linux进程(除0号进程)都是通过 0号进程 fork 而来,这个 0 号进程是所有进程的始祖。

首先你要确保,你的已经安装了 pecl (如果没有安装过拓展,可以查一下我之前的文章教学),且代码执行在 linux php-cli 模式下。

PHP 也是提供了类C 函数,我们这次学习需要使用的函数如下:

2.1 创建子进程

pcntl_fork ( void ) : int

成功时,在父进程执行线程内返回产生的子进程的PID,在子进程执行线程内返回0。失败时,在 父进程上下文返回-1,不会创建子进程,并且会引发一个PHP错误。

文档栗子:

<?php

$pid = pcntl_fork();
//父进程和子进程都会执行下面代码
if ($pid == -1) {
    //错误处理:创建子进程失败时返回-1.
     die('could not fork');
} else if ($pid) {
     //父进程会得到子进程号,所以这里是父进程执行的逻辑
     pcntl_wait($status); //等待子进程中断,防止子进程成为僵尸进程。
} else {
     //子进程得到的$pid为0, 所以这里是子进程执行的逻辑。
}

?>

大概讲解一下,这个函数是PHP用来创建子进程的,如果成功返回值是子进程的进程ID,如果返回 -1 ,则代表创建子进程失败,如果是返回 0 ,说明创建子进程成功,但是此时执行的是父进程的代码,进程间是独立的,我们没办法去控制哪个进程先执行。当 cpu 的时间片分给了它,它就会执行,除非我们使用阻塞的代码去强制让其指定顺序的执行。因此在这种情况我们可以使用 返回值来进行判断当前执行的是父进程代码还是子进程代码。

2.2 获取当前的进程ID

getmypid ( void ) : int

函数会直接返回当前代码执行进程的进程 ID

2.3 监听进程

pcntl_waitpid ( int $pid , int &$status [, int $options = 0 [, array &$rusage ]] ) : int

pcntl_waitpid()返回退出的子进程进程号,发生错误时返回-1,如果提供了 WNOHANG作为option(wait3可用的系统)并且没有可用子进程时返回0。

这个我们是用来监听子进程是否执行结束使用的,这里我们只是使用了其中一种方式,更多对该函数的使用可以自己研究。

2.4 写一个较为完整的栗子

<?php

namespace Test;

class Test
{
    /** @var int 默认是 10 个进程 */
    protected $processNums;

    /** @var int 子进程时间片 */
    const CHILD_RUN = 0;

    /** @var int 失败 */
    const FORK_ERROR = -1;


    public function __construct(int $processNums = 10)
    {
        $this->processNums = $processNums;
    }

    public function run()
    {

        for ($i = 1; $i <= $this->processNums; $i++) { // 创建指定的子进程个数,这里从 1 开始方便计数
            $pid = pcntl_fork();
            $this->dispatchInfo("当前进程:" . getmypid() . ' fork的子进程:' . $pid);
            switch ($pid) {
                case self::FORK_ERROR:
                    $this->dispatchInfo('子进程创建失败');
                    break;
                case self::CHILD_RUN:
                    //子进程
                    $this->dispatchInfo("当前子进程:" . getmypid() . ' ' . "  子进程执行任务");
                    // 下面写你要实现的方法
                    exit(0);//执行完就退出把!
                    break;
                default:
                    //父进程执行过程
                    $this->dispatchInfo("当前进程:" . getmypid() . ' ' . "   开启第{$i}个子进程{$pid}");
                    $childPids[] = $pid;
                    break;
            }
        }

        while (count($childPids) > 0) {
            foreach ($childPids as $key => $pid) {
                $res = pcntl_waitpid($pid, $status, WNOHANG);// 非阻塞模式,让所有子进程都能被监听
                if ($res == -1 || $res > 0) { //这里我也奇怪,官方文档的栗子也是这么判断子进程已经结束的
                    $this->dispatchInfo("   子进程{$pid}关闭...");
                    unset($childPids[$key]);//剔除已关闭的子进程
                }
            }

            if (count($childPids) == 0) { // 全都跑完了
                break;
            }
        }
        $this->dispatchInfo('全部任务结束,主进程退出');
    }

    protected function dispatchInfo(string $info)
    {
        echo date("Y-m-d H:i:s") . '  ' . $info . PHP_EOL;
    }

}

3.僵尸进程和孤儿进程的产生

  子进程的结束和父进程的运行是一个异步过程,即父进程永远无法预测子进程 到底什么时候结束。 当一个 进程完成它的工作终止之后,它的父进程需要调用wait()或者waitpid()系统调用取得子进程的终止状态。并且通过这种方式去回收对应的资源。

这里我们解释一下这两种进程的产生以及危害。

3.1 孤儿进程

我们知道,每个用户态进程都是由它爸爸生出来的,如果爸爸死了,那它就变成了孤儿了。变成孤儿会有啥问题呢?

其实也没啥问题,因为会有人来领养它,有一个进程 叫 init进程(1号进程), 它是一个好人,专门领养孤儿进程,让这些孤儿可以寿终正寝。因此本质上来说,如果父进程被杀了,也不会对系统造成太大的影响。

3.2 僵尸进程

僵尸,是一个恐怖的存在,死不了啊,不死就占地方,地球就这么大,又没有无限的容量。什么情况下会产生僵尸进程呢?

就是死神罢工了,这里的死神其实就是父进程。子进程执行完后,会留下一个进程标识符,等待父进程去回收,如果父进程没有回收,那么这个进程标识符(进程ID)就会一直被占用,就好比说厕所上完了不开门,不给别人去用。

为了区分开孤儿进程和僵尸进程的产生原因,我们可以这么想,子进程在 exit 之后,父进程应该主动的去接受信号释放子进程的进程标志符,也就是前面的 pcntl_waitpid 函数,如果我们通过父进程创建子进程,却没有主动去释放的话,那么僵尸进程必然产生。此时父进程和子进程都可以正常的结束,但是子进程的进程标识符无法被系统释放。

然后我们再想想另一种情况,父进程被 kill 掉了,会发生什么情况,我们前面说没有父进程,子进程就变成了孤儿进程,会被接管。假设父进程挂了,但是子进程都还没结束,那么他们都是孤儿进程

PS: 多进程在有些时候很好用,但是也有一些弊端,这里也只是简单解密了其中一种方式。没有涉及到进程通信等问题。其实要考虑的东西是可以有很多的,希望大家在合适的场合合适的使用。切不可滥用。

上一篇:PHP多进程 (3)信号


下一篇:linux系统宝塔面板如何解除PHP禁用函数?