深入探究PHP的多进程编程方法

5年以前  |  阅读数:1081 次  |  编程语言:PHP 

子进程的创建
一般的子进程的写法是:


    <?php
    $pid = pcntl_fork();
    if($pid == -1){
         //创建失败
         die('could not fork');
    }
    else{
        if($pid){
            //从这里开始写的代码是父进程的
            exit("parent!");
        }
        else{
            //子进程代码,为防止不停的启用子进程造成系统资源被耗尽的情况,一般子进程代码运行完成后,加入exit来确保子进程正常退出。
            exit("child");
        }
    }
    ?>

上边的代码如果创建子进程成功的话,系统就有了2个进程,一个为父进程,一个为子进程,子进程的id号为$pid。在系统运行到$pid = pcntl_fork();时,在这个地方进行分支,父子进程各自开始运行各自的程序代码。代码的运行结果是parent 和child,很奇怪吧,为什么一个if和else互斥的代码中,都输出了结果?其实是像上边所说的,代码在pcntl_fork时,一个父进程运行parent,一个子进程运行了child。在代码结果上就显示了parent和child。至于谁先谁后的问题,这得要看系统资源的分配了。

如果需要起多个进程来处理数据,可以根据数据的数量,按照约定好的数量比如说1000条一个进程来起子进程。使用for循环就可以了。   

     #如果获得的总数小于或等于0,等待60秒,并退出
      if ($count <= 0) 
      {
        sleep(60);
        exit;
      }
      #如果大于1000,计算需要起的进程数
      if ($count > 1000)
      {
        $cycleSize = ceil($count/1000);
      }
      else
      {
        $cycleSize = 1;
      }

      for ($i=0; $i<$cycleSize; $i++)
      {
        $pid  = pcntl_fork();
        if($pid == -1)
        {
          break;
        }
        else
        {
          if($pid)
          {
            #父进程获得子进程的pid,存入数组
            $pidArr[] = $pid;
          }
          else
          {
            //开始发送,子进程执行完自己的任务后,退出。
              exit;
          }
        }
      }

      while(count($pidArr) > 0)
      {
        $myId  = pcntl_waitpid(-1, $status, WNOHANG);
        foreach($pidArr as $key => $pid)
        {
          if($myId == $pid) unset($pidArr[$key]);
        }
      }
然后使用crontab,来使此PHP程序每隔一段时间自动执行。

当然,示例代码比较简单,具体还需要考虑怎么防止多个子进程执行到同一条数据或者当前进程处理数据未完成时,crontab又开始执行PHP文件启用新的进程等等。

PHP多进程实现方式
下面来系统地整理一下PHP多进程的实现方式:

1. 直接方式

pcntl_fork() 创建一个进程,在父进程返回值是子进程的pid,在子进程返回值是0,-1表示创建进程失败。跟C非常相似。

测试脚本 test.php


    <?php
      // example of multiple processes
      date_default_timezone_set( 'Asia/Chongqing');
      echo "parent start, pid ", getmypid(), "\n" ;
      beep();
      for ($i=0; $i<3; ++$i){
         $pid = pcntl_fork();
          if ($pid == -1){
             die ("cannot fork" );
         } else if ($pid > 0){
             echo "parent continue \n";
             for ($k=0; $k<2; ++$k){
               beep();
            }
         } else if ($pid == 0){
             echo "child start, pid ", getmypid(), "\n" ;
             for ($j=0; $j<5; ++$j){
               beep();
            }
             exit ;
         }
      }
      // ***
      function beep(){
          echo getmypid(), "\t" , date( 'Y-m-d H:i:s', time()), "\n" ;
         sleep(1);
      }
    ?>

用命令行运行


    #php -f test.php

输出结果


    parent start, pid 1793
    1793  2013-01-14 15:04:17
    parent continue
    1793  2013-01-14 15:04:18
    child start, pid 1794
    1794  2013-01-14 15:04:18
    1794  2013-01-14 15:04:19
    1793  2013-01-14 15:04:19
    1794  2013-01-14 15:04:20
    parent continue
    1793  2013-01-14 15:04:20
    child start, pid 1795
    1795  2013-01-14 15:04:20
    17931794        2013-01-14 15:04:212013-01-14 15:04:21

    1795  2013-01-14 15:04:21
    1794  2013-01-14 15:04:22
    1795  2013-01-14 15:04:22
    parent continue
    1793  2013-01-14 15:04:22
    child start, pid 1796
    1796  2013-01-14 15:04:22
    1793  2013-01-14 15:04:23
    1796  2013-01-14 15:04:23
    1795  2013-01-14 15:04:23
    1795  2013-01-14 15:04:24
    1796  2013-01-14 15:04:24
    1796  2013-01-14 15:04:25
    1796  2013-01-14 15:04:26

从中看到,创建了3个子进程,和父进程一起并行运行。其中有一行格式跟其他有些不同,
17931794 2013-01-14 15:04:212013-01-14 15:04:21
因为两个进程同时进行写操作,造成了冲突。

2. 阻塞方式

用直接方式,父进程创建了子进程后,并没有等待子进程结束,而是继续运行。似乎这里看不到有什么问题。如果php脚本并不是运行完后自动结束,而是常驻内存的,就会造成子进程无法回收的问题。也就是僵尸进程。可以通过pcntl_wai()方法等待进程结束,然后回收已经结束的进程。
将测试脚本改成:


    $pid = pcntl_fork();
    if ($pid == -1){
      ...
    } else if ($pid > 0){
       echo "parent continue \n";
       pcntl_wait($status);
       for ($k=0; $k<2; ++$k){
         beep();
      }
    } else if ($pid == 0){
       ...
    }

用命令行运行


    #php -f test.php

输出结果


    parent start, pid 1807
    1807  2013-01-14 15:20:05
    parent continue
    child start, pid 1808
    1808  2013-01-14 15:20:06
    1808  2013-01-14 15:20:07
    1808  2013-01-14 15:20:08
    1808  2013-01-14 15:20:09
    1808  2013-01-14 15:20:10
    1807  2013-01-14 15:20:11
    1807  2013-01-14 15:20:12
    parent continue
    child start, pid 1809
    1809  2013-01-14 15:20:13
    1809  2013-01-14 15:20:14
    1809  2013-01-14 15:20:15
    1809  2013-01-14 15:20:16
    1809  2013-01-14 15:20:17
    1807  2013-01-14 15:20:18
    1807  2013-01-14 15:20:19
    child start, pid 1810
    1810  2013-01-14 15:20:20
    parent continue
    1810  2013-01-14 15:20:21
    1810  2013-01-14 15:20:22
    1810  2013-01-14 15:20:23
    1810  2013-01-14 15:20:24
    1807  2013-01-14 15:20:25
    1807  2013-01-14 15:20:26

父进程在pcntl_wait()将自己阻塞,等待子进程运行完了才接着运行。

3. 非阻塞方式

阻塞方式失去了多进程的并行性。还有一种方法,既可以回收已经结束的子进程,又可以并行。这就是非阻塞的方式。
修改脚本:


    <?php
      // example of multiple processes
      date_default_timezone_set( 'Asia/Chongqing');
      declare (ticks = 1);
      pcntl_signal(SIGCHLD, "garbage" );
      echo "parent start, pid ", getmypid(), "\n" ;
      beep();
      for ($i=0; $i<3; ++$i){
         $pid = pcntl_fork();
          if ($pid == -1){
             die ("cannot fork" );
         } else if ($pid > 0){
             echo "parent continue \n";
             for ($k=0; $k<2; ++$k){
               beep();
            }
         } else if ($pid == 0){
             echo "child start, pid ", getmypid(), "\n" ;
             for ($j=0; $j<5; ++$j){
               beep();
            }
             exit (0);
         }
      }
      // parent
      while (1){
          // do something else
         sleep(5);
      }
      // ***
      function garbage($signal){
          echo "signel $signal received\n" ;

          while (($pid = pcntl_waitpid(-1, $status, WNOHANG))> 0){
             echo "\t child end pid $pid , status $status\n" ;
         }
      }
      function beep(){
          echo getmypid(), "\t" , date( 'Y-m-d H:i:s', time()), "\n" ;
         sleep(1);
      }
    ?>

用命令行运行


    #php -f test.php &

输出结果


    parent start, pid 2066
    2066  2013-01-14 16:45:34
    parent continue
    2066  2013-01-14 16:45:35
    child start, pid 2067
    2067  2013-01-14 16:45:35
    20662067        2013-01-14 16:45:362013-01-14 16:45:36

    2067  2013-01-14 16:45:37
    parent continue
    2066  2013-01-14 16:45:37
    child start, pid 2068
    2068  2013-01-14 16:45:37
    2067  2013-01-14 16:45:38
    2068  2013-01-14 16:45:38
    2066  2013-01-14 16:45:38
    parent continue
    2066  2013-01-14 16:45:40
    child start, pid 2069
    2069  2067  2013-01-14 16:45:40
    2013-01-14 16:45:40
    2068  2013-01-14 16:45:40
    2066  2013-01-14 16:45:41
    2069  2013-01-14 16:45:41
    2068  2013-01-14 16:45:41
    signel 17 received
         child end pid 2067, status 0
    2069  2013-01-14 16:45:42
    2068  2013-01-14 16:45:42
    2069  2013-01-14 16:45:43
    signel 17 received
         child end pid 2068, status 0
    2069  2013-01-14 16:45:44
    signel 17 received
         child end pid 2069, status 0

多个进程又并行运行了,而且运行大约10秒钟之后,用 ps -ef | grep php 查看正在运行的进程,只有一个进程
lqling 2066 1388 0 16:45 pts/1 00:00:00 php -f t5.php
是父进程,子进程被回收了。

子进程退出状态


    pcntl_waitpid(-1, $status, WNOHANG) $status

返回子进程的结束状态

windows下多线程

windows系统不支持pcntl函数,幸好有curl_multi_exec()这个工具,利用内部的多线程,访问多个链接,每个链接可以作为一个任务。

编写脚本 test1.php


    <?php
      date_default_timezone_set( 'Asia/Chongqing');
      $tasks = array(
         'http://localhost/feedbowl/t2.php?job=task1',
         'http://localhost/feedbowl/t2.php?job=task2',
         'http://localhost/feedbowl/t2.php?job=task3'
      );
      $mh = curl_multi_init();
      foreach ($tasks as $i => $task){
         $ch[$i] = curl_init();
         curl_setopt($ch[$i], CURLOPT_URL, $task);
         curl_setopt($ch[$i], CURLOPT_RETURNTRANSFER, 1);
         curl_multi_add_handle($mh, $ch[$i]);
      }
      do {$mrc = curl_multi_exec($mh,$active); } while ($mrc == CURLM_CALL_MULTI_PERFORM);
      while ($active && $mrc == CURLM_OK) {
         if (curl_multi_select($mh) != -1) {
          do {$mrc = curl_multi_exec($mh, $active); } while ($mrc == CURLM_CALL_MULTI_PERFORM);
         }
      }
      // completed, checkout result
      foreach ($tasks as $j => $task){
         if (curl_error($ch[$j])){
           echo "task ${j} [$task ] error " , curl_error($ch[$j]), "\r\n" ;
         } else {
           echo "task ${j} [$task ] get: \r\n" , curl_multi_getcontent($ch[$j]), "\r\n" ;
         }
      }
    ?>

编写脚本 test2.php


    <?php
      date_default_timezone_set( 'Asia/Chongqing');
      echo "child start, pid ", getmypid(), "\r\n" ;
      for ($i=0; $i<5; ++$i){
         beep();
      }
      exit (0);
      // ***
      function beep(){
        echo getmypid(), "\t" , date('Y-m-d H:i:s' , time()), "\r\n";
        sleep(1);
      }
    ?>

用命令行运行


    #php -f test1.php &

输出结果


    task 0 [http://localhost/feedbowl/t2.php?job=task1] get:
    child start, pid 5804
    5804  2013-01-15 20:22:35
    5804  2013-01-15 20:22:36
    5804  2013-01-15 20:22:37
    5804  2013-01-15 20:22:38
    5804  2013-01-15 20:22:39

    task 1 [http://localhost/feedbowl/t2.php?job=task2] get:
    child start, pid 5804
    5804  2013-01-15 20:22:35
    5804  2013-01-15 20:22:36
    5804  2013-01-15 20:22:37
    5804  2013-01-15 20:22:38
    5804  2013-01-15 20:22:39

    task 2 [http://localhost/feedbowl/t2.php?job=task3] get:
    child start, pid 5804
    5804  2013-01-15 20:22:35
    5804  2013-01-15 20:22:36
    5804  2013-01-15 20:22:37
    5804  2013-01-15 20:22:38
    5804  2013-01-15 20:22:39

从打印的时间看到,多个任务几乎是同时运行的。

 相关文章:
PHP分页显示制作详细讲解
SSH 登录失败:Host key verification failed
获取IMSI
将二进制数据转为16进制以便显示
获取IMEI
文件下载
贪吃蛇
双位运算符
PHP自定义函数获取搜索引擎来源关键字的方法
Java生成UUID
发送邮件
年的日历图
提取后缀名
在Zeus Web Server中安装PHP语言支持
让你成为最历害的git提交人
Yii2汉字转拼音类的实例代码
再谈PHP中单双引号的区别详解
指定应用ID以获取对应的应用名称
Python 2与Python 3版本和编码的对比
php封装的page分页类完整实例