基于PCNTL的PHP并发编程

原创文章,转载请注明出处: http://huyanping.sinaapp.com/?p=178
作者: Jenner

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
$pid =pcntl_fork();
 
switch ( $pid ){
case -1:
     echo "couldn't fork" ;
     break ;
case 0:
     echo "I'm parent" ;
     break ;
default :
     echo "I'm child" ;
}
?>

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
while (true){
     $pid = pcntl_fork();
     switch ( $pid ){
     case -1:
         echo "couldn't fork" ;
         break ;
     case 0:{
         $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。这对实生产者和消费者模型造成一定难度。以下是一个生产者消费者的模型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
<?php
/**
  * @author:Jenner
  * @date 2014-01-14
  */
class JetMultiProcess {
 
     //最大队列长度
     private $size ;
 
     private $curSize ;
 
     //生产者
     private $producer ;
 
     //消费者
     private $worker ;
 
     private $queueName ;
 
     private $httpsqs ;
 
     /**
      * 构造函数
      * @param string $worker 需要创建的消费者类名
      * @param int $size 最大子进程数量
      * @param $producer 需要创建的消费者类名
      */
     public function __construct( $producer $worker $size =10){
         $this ->producer =  new $producer ;
         $this ->worker =  $worker ;
         $this ->size =  $size ;
         $this ->curSize = 0;
     }
 
     public function start(){
 
         $producerPid = pcntl_fork();
         if ( $producerPid == -1) {
             die ( "could not fork" );
         else if ( $producerPid ) { // parent
 
             while (true){
                 $pid = pcntl_fork();
                 if ( $pid == -1) {
                     die ( "could not fork" );
                 else if ( $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方法中创建死循环。循环写入和读取队列进行协同工作。该类没有提供进程间通信的功能。通信需要在生产者和消费者类中实现。这样能够使得进程控制的代码看起来更加简洁。

你可能感兴趣的:(PHP,并发,pcntl)