PHP多进程初步

一、前言

我们都知道PHP是单线程执行,处理多并发主要是依赖服务器或PHP-FPM的多进程及它们进程的复用,但PHP实现多进程也意义重大,尤其是在后台Cli模式下处理大量数据或运行后台DEMON守护进程时。不能应用在Web服务器环境。

/** 检测是否CLI模式,确保这个函数只能运行在SHELL中 */
if (substr(php_sapi_name(), 0, 3) !== 'cli') {
  die("cli mode only");
}

日常任务中,有时需要通过php脚本执行一些日志分析,队列处理等任务,当数据量比较大时,可以使用多进程来处理。

PHP的多线程也曾被人提及,但进程内多线程资源共享和分配的问题难以解决。PHP也有多线程想关的扩展 pthreads ,但据说不太稳定,且要求环境为线程安全,所用不多。

要实现PHP的多进程,需要安装 pcntl 和 posix 扩展。

二、创建子进程

使用 pcntl_fork() 函数可以在当前位置产生分支。fork 是创建了一个子进程,父进程和子进程都从 fork 的位置开始向下继续执行,不同的是父进程执行过程中,得到的 fork 返回值为子进程号,而子进程得到的是0,执行失败则返回-1。

因为系统初始init进程的pid为1,后来的所有进程pid都会大于该进程,所以可以通过 pcntl_fork() 的返回值大于1来判断当前进程是父进程,返回值等于0来判断是子进程。

$ppid = posix_getpid(); // 获取当前进程的id
$pid = pcntl_fork();  // 创建子进程
if ($pid == -1) {
    throw new Exception('fork子进程失败!');
} elseif ($pid > 0) {
    // 父进程执行逻辑
    cli_set_process_title("我是父进程,我的进程id是{$ppid}.");
    sleep(30); 
    pcntl_wait($status); //等待子进程中断,防止子进程成为僵尸进程。
} else {
    // 子进程执行逻辑
    $cpid = posix_getpid();
    cli_set_process_title("我是{$ppid}的子进程,我的进程id是{$cpid}.");
    sleep(30);
}

执行结果:

注意:如果是在循环中创建子进程,那么子进程中最后要 exit 退出,防止子进程进入循环。

三、管理子进程

管理子进程,使用的是信号。简单来说,就是父进程里使用两个函数 pcntl_signal() 和 pcntl_signal_dispatch(),负责给子进程安装信号处理器和分发工作。

在计算机科学中,信号是Unix、类Unix以及其他POSIX兼容的操作系统中进程间通讯的一种有限制的方式。它是一种异步的通知机制,用来提醒进程一个事件已经发生。

我们通过在父进程接收子进程传来的信号,判断子进程状态,来对子进程进行管理。我们需要在父进程里使用 pcntl_signal() 函数和 pcntl_signal_dispatch() 函数来给各个子进程安装信号处理器:

// 安装一个信号处理器,$signo是待处理的信号常量,callback是其处理函数
pcntl_signal (int $signo , callback $handler) 
// 调用每个等待信号通过pcntl_signal()安装的处理器 pcntl_signal_dispatch ()

PHP内常见的信号常量有:

  • SIGCHLD:子进程退出成为僵尸进程会向父进程发送此信号
  • SIGHUP:进程挂起
  • SIGTEM:进程终止

四、处理子进程

处理子进程,需要两个函数:

// 向进程id为$pid的进程发送$sig信号
bool posix_kill ( int $pid, int $sig )
//挂起当前进程的执行直到进程号为$pid的进程退出(如果$pid为-1,则等待任意一个子进程) int pcntl_waitpid ( int $pid, int &$status [, int $options = 0 ] )

posix_kill() 函数通过向子进程发送一个信号来操作子进程,在需要要时可以选择给子进程发送进程终止信号来终止子进程;

pcntl_waitpid() 函数等待或返回 fork 的子进程状态,如果指定的子进程在此函数调用时已经退出(俗称僵尸进程),此函数将立刻返回,并释放子进程的所有系统资源,此进程可以避免子进程变成僵尸进程,造成系统资源浪费。这样就可以实现跟子进程共同完成的任务的目的了。

五、实例

如果一个任务被分解成多个进程执行,就会减少整体的耗时。比如有一个比较大的数据文件要处理,这个文件由很多行组成。如果单进程执行要处理的任务,量很大时要耗时比较久。这时可以考虑多进程。多进程处理分解任务,每个进程处理文件的一部分,这样需要均分割一下这个大文件成多个小文件(进程数和小文件的个数等同就可以)。

比如文件 file.log 有10万行数据,现在想分4个进程处理。需要分割2.5万行一个文件。命令 split 可以做到:

<?php

shell_exec('split -l 25000 -d file.log prefix_name');

// 3个子进程处理任务
for ($i = 0; $i < 3; $i++){
    $pid = pcntl_fork();

    if ($pid == -1) {
        die("could not fork");
    } elseif ($pid) {
        echo "I'm the Parent $i
";
    } else {// 子进程处理
        $content = file_get_contents("prefix_name0".$i);
        // 业务处理 begin

        // 业务处理 end
        exit; // 一定要注意退出子进程,否则pcntl_fork()会被子进程再fork,带来处理上的影响。
    }
}

// 等待子进程执行结束
while (pcntl_waitpid(0, $status) != -1) {
    $status = pcntl_wexitstatus($status);
    echo "Child $status completed
";
}

参考:

《php多进程总结》:https://www.cnblogs.com/leezhxing/p/5223289.html

《初探PHP多进程》:https://www.cnblogs.com/zhenbianshu/p/5676822.html

《PHP利用多进程处理任务》:https://www.cnblogs.com/firstForEver/p/7301630.html

原文地址:https://www.cnblogs.com/tangxuliang/p/9208133.html