PHP多进程初探 --- 开篇

实际上PHP是有多线程的,只是很多人不常用。

使用PHP的多线程首先需要下载安装一个线程安全版本(ZTS版本)的PHP,然后再安装pecl的pthread扩展

实际上PHP是有多进程的,有一些人再用,总体来说 php 的多进程还算凑合,只需要在安装 PHP 的时候开启 pcntl 模块(是不是跟UNIX中的fcntl 有点儿.... ....)即可。在 *NIX 下,在终端命令行下使用 php -m 就可以看到是否开启了 pcntl 模块。

所以我们只说 php 的多进程,至于 php多线程 就暂时放到一边儿。

 

注意:不要在 apache 或者 fpm 环境下使用 php多进程,这将会产生不可预估的后果。

 

进程是程序执行的实例,举个例子有个程序叫做 “ xxoo病毒.exe ”,这个程序平时是以文件形式存储在硬盘上,当你双击运行后,就会形成一个该程序的进程。

系统会给每一个进程分配一个唯一的非负整数用来标记进程,这个数字称作进程ID。

当该进程被杀死或终止后,其进程ID就会被系统回收,然后分配给新的其余的进程。

说了这么多,这鬼东西有什么用吗?

我平时用 CI、YII 写个 CURD 跟这个也没啥关联啊。

实际上,如果你了解 APACHE PHP MOD 或者 FPM 就知道这些东西就是多进程实现的。

以 FPM 为例,一般都是 nginx 作为 http 服务器挡在最前面,静态文件请求则 nginx 自行处理,遇到 php 动态请求则转发给 php-fpm 进程来处理。

如果你的 php-fpm 配置只开了 5 个进程,如果处理任意一个用户的请求都需要1秒钟,那么 5 个 fpm 进程 1 秒中就最多只能处 5 个用户的请求。

所以结论就是:如果要单位时间内干活更快更多,就需要更多的进程,总之一句话就是多进程可以加快任务处理速度。

在 php 中我们使用 pcntl_fork() 来创建多进程(在 *NIX 系统的 C 语言编程中,已有进程通过调用 fork 函数来产生新的进程)。fork出来新进程则成为子进程,原进程则成为父进程,子进程拥有父进程的副本。

这里要注意:

  • 子进程与父进程共享程序正文段
  • 子进程拥有父进程的数据空间和堆、栈的副本,注意是副本,不是共享
  • 父进程和子进程将继续执行 fork 之后的程序代码
  • fork 之后,是父进程先执行还是子进程先执行无法确认,取决于系统调度(取决于信仰)

这里说子进程拥有父进程数据空间以及堆、栈的副本,实际上,在大多数的实现中也并不是真正的完全副本。

更多是采用了COW(Copy On Write)即写时复制的技术来节约存储空间。

简单来说,如果父进程和子进程都不修改这些 数据、堆、栈 的话,那么父进程和子进程则是暂时共享同一份 数据、堆、栈。只有当父进程或者子进程试图对 数据、堆、栈 进行修改的时候,才会产生复制操作,这就叫做写时复制。

在调用完 pcntl_fork() 后,该函数会返回两个值。

父进程中返回子进程的进程ID,在子进程内部本身返回数字0

由于多进程在 apache 或者 fpm 环境下无法正常运行,所以大家一定要在php cli环境下执行下面php代码。

 

第一段代码,我们来说明在程序从 pcntl_fork() 后父进程和子进程将各自继续往下执行代码:

<?php

$pid = pcntl_fork();

if ($pid > 0) {
    // 父进程中返回子进程ID,进程ID为一个非负整数
    echo "我是父亲" . PHP_EOL;
} elseif (0 == $pid) {
    // 子进程中返回0
    echo "我的儿子" . PHP_EOL;
} else {
    echo "fork失败" . PHP_EOL;
}

将文件保存为 test.php,然后在使用 cli 执行,结果如下图所示:

$ php fork.php
我是父亲
我的儿子

 

第二段代码,用来说明子进程拥有父进程的数据副本,而并不是共享:

<?php 

$number = 1;

$pid = pcntl_fork();

if ($pid > 0) {
    $number += 1;
    echo "我是父亲,number+1={$number}" . PHP_EOL;
} else if (0 == $pid) {
    $number += 2;
    echo "我是儿子,number+2={$number}" . PHP_EOL;
} else {
    echo "fork失败" . PHP_EOL;
}

使用 cli 执行:

$ php fork.php
我是父亲,number+1=2
我是儿子,number+2=3

 

第三段代码,比较容易让人思维混乱,pcntl_fork() 配合 for 循环来做些东西,问题来了:会显示几次 “ 儿子 ”?

<?php 

for ($i = 1; $i <= 3; $i++) {
    $pid = pcntl_fork();
    if ($pid > 0) {
        // do nothing ...
    } elseif (0 == $pid) {
        echo "儿子" . PHP_EOL;
    }
}

上面代码执行结果如下:

$ php fork.php
儿子
儿子
儿子
儿子
儿子
儿子
儿子

仔细数数,竟然是显示了7次 “ 儿子 ”。

好奇怪,难道不是3次吗?... ...

下面我修改一下代码,结合下面的代码,再思考一下为什么会产生7次而不是3次。

<?php

for ($i = 1; $i <= 3; $i++) {
    $pid = pcntl_fork();
    if ($pid > 0) {
        // do nothing...
    } elseif (0 == $pid) {
        echo "儿子" . PHP_EOL;
        exit;
    }
}

执行结果如下图所示:

$ php fork.php
儿子
儿子
儿子

前面强调过:父进程和子进程将继续执行fork之后的程序代码。这里就不解释,实在想不明白的,可以动手自己画画思考一下。

为了避免写成臭尾理论文儿,这里强行断篇分割一下,下一章说僵尸进程和孤儿进程的一些恩怨情仇。

评论