PHP是一门较早出现的WEB开发脚本语言,并由于其语法结构简单、易学、开源等特性迅速占领WEB开发脚本语言领域,并成为这个领域的龙头老大直至今日。PHP从一出生就被设计用来快速开发WEB应用,这也注定了它在某些方面的先天不足,例如在cli环境下处理大量数据的情况,或者在并发编程方面,都显得力不从心。

本文主要讲解基于PCNTL的PHP并发编程,虽然PHP本身不支持多进程,但基于Linux的PHP扩展PCNTL却可以提供多进程编程。网络上很多同类文章,但笔者进行多次尝试后发现,不是难以控制进程数量,就是有潜在产生僵尸进程或孤儿进程的危险,或者父进程阻塞难以获得更大的并发效果,且大多没有介绍FORK的原理,使得PHP程序员学习PCNTL并发编程尤为困难。本文力求解决这个问题。

FORK编程的大概原理是,每次调用fork函数,操作系统就会产生一个子进程,儿子进程所有的堆栈信息都是原封不动复制父进程的,而在fork之后,父进程与子进程实际上是相互独立的,父子进程不会相互影响。也就是说,fork调用位置之前的所有变量,父进程和子进程是一样的,但fork之后则取决于各自的动作,且数据也是独立的;因为数据已经完整的复制给了子进程。而唯一能够区分父子进程的方法就是判断fork的返回值。如果为0,表示是子进程,如果为正数,表示为父进程,且该正数为子进程的PID(进程号),而如果是-1,表示子进程创建失败。请看如下代码:

<?php

$pid=pcntl_fork();

switch($pid){

case-1:

echo”couldn’t fork”;

break;

case0:

echo”I’m parent”;

break;

default:

echo”I’m child”;

}

?>

以上代码会产生一个子进程,并根据返回的pid进行父子进程的判断。

生产环境中以上代码大多是不适用的,我们需要大量的并发进程为同时为我们处理事情。这时,我们就需要fork多次,而产生的子进程数量需要在我们的控制之中,否则无限制的fork只会拖垮服务器。笔者曾经有过经历,几秒钟服务器负载从0.3左右飙到800多,吓的一身冷汗。

而子进程的使用通常会涉及到两种:子进程执行完任务直接退出;子进程常驻内存,等待任务。以上两种方式适用于不同情况。第一种情况大多我们不需要考虑太多,除非子进程的创建是循环进行的。而第二种则需要考虑进程间通信。

无论哪一种,无可避免的一个问题就是僵尸进程。僵尸进程就是子进程退出后,父进程没有及时回收,系统仍然保留子进程的执行信息(例如PID,退出状态等),留待其他程序读取。如果僵尸进程数量很少,我们可以忽略掉。但如果是在一个循环中fork(并发编程中常见的死循环),这个问题就不能无视了,父进程必须定期回收已经退出的子进程。子进程的回收我们采用pcnt_wait函数来完成。如下面这段代码:

<?php

while(true){

$pid= pcntl_fork();

switch($pid){

case-1:

echo”couldn’t fork”;

break;

case0:{

$subPid= pcntl_wait($status);

var_dump($status);

break;

}

default:

echo”I’m child”;

exit(0);

}

}

以上代码能够循环产生子进程,并且父进程会阻塞等待子进程退出,这样就产生了一个问题,父进程必须等待一个子进程退出后,再创建另外一个。额,这还是串行执行的不是吗?是的,解决办法就是将pcntl_wait函数替换成pcntl_waitpid()并添加WNOHANG参数。该函数可以在没有子进程退出的情况下立刻跳出执行后续代码。能够达到更好的并发效果。具体代码这里不再演示。

循环创建子进程是一件非常浪费操作系统资源的事情。既然使用了死循环来处理任务,那么就说明任务是一个可以队列化的数据结构。我们可以采用进程间的通信,解决子进程退出重建的问题。而通信的机制主要有信号量、管道、共享内存等。然后我们需要一个生产者和消费者的模型。而基于fork的这种代码编写方式,非常不利于我们编写复杂的业务逻辑。所以建议将进程控制与业务处理的代码进程抽象隔离。进程间通信本文暂不涉及,如果读者有需要可以阅读关于管道、共享内存和信号量的文章。

根据上面所说,循环创建子进程会造成系统资源的浪费,而循环创建往往意味着任务可以队列化。我们可以创建子进程后,让子进程常驻内存,持续执行等待任务到达。而这类模型往往可以用生产-消费模型来实现。生产者负责将任务写入队列,而子进程从队列中取出任务并执行。队列的实现最好采用本身支持互斥的方式,这样可以降低代码的复杂度,管道是个不错的选择。

基于fork方式实现的多进程,由于我们只能使用Pid来做代码隔离,所以进程控制中会充斥的各种if、else或者switch。这对实生产者和消费者模型造成一定难度。以下是一个生产者消费者的模型:

<?php

/**

* @author:Jenner

* @date 2014-01-14

*/

classJetMultiProcess {

//最大队列长度

private$size;

private$curSize;

//生产者

private$producer;

//消费者

private$worker;

private$queueName;

private$httpsqs;

/**

* 构造函数

* @param string $worker 需要创建的消费者类名

* @param int $size 最大子进程数量

* @param $producer 需要创建的消费者类名

*/

publicfunction
__construct($producer, $worker, $size=10){

$this->producer = new$producer;

$this->worker = $worker;

$this->size = $size;

$this->curSize = 0;

}

publicfunction
start(){

$producerPid= pcntl_fork();

if($producerPid== -1) {

die(“could not fork”);

} elseif
($producerPid) {// parent

while(true){

$pid= pcntl_fork();

if($pid== -1) {

die(“could not fork”);

} elseif
($pid) {// parent

$this->curSize++;

if($this->curSize>=$this->size){

$sunPid= pcntl_wait($status);

}

} else{// worker

$worker= new$this->worker;

$worker->run();

exit();

}

}

} else{// producer

$this->producer->run();

exit();

}

}

}

以上代码,通过size控制多进程数量,通过构造函数传入生产者和消费者的类型。父进程第一次fork产生一个子进程生产者,然后再进行size次fork创建多个消费者。类似方法可以创建多个生产者和多个消费者协同工作。生产者和消费者都必须实现run方法,并在run方法中创建死循环。循环写入和读取队列进行协同工作。该类没有提供进程间通信的功能。通信需要在生产者和消费者类中实现。这样能够使得进程控制的代码看起来更加简洁。