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


Posted in PHP onAugust 18, 2015

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

<?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 相关文章推荐
浅析PHP水印技术
Feb 14 PHP
PHP去掉从word直接粘贴过来的没有用格式的函数
Oct 29 PHP
PHP常用的文件操作函数经典收藏
Apr 02 PHP
简单的cookie计数器实现源码
Jun 07 PHP
浅析PHP程序设计中的MVC编程思想
Jul 28 PHP
PHP函数实现从一个文本字符串中提取关键字的方法
Jul 01 PHP
php获得客户端浏览器名称及版本的方法(基于ECShop函数)
Dec 23 PHP
非常重要的php正则表达式详解
Jan 04 PHP
php readfile下载大文件失败的解决方法
May 22 PHP
thinkPHP5使用Rabc实现权限管理
Aug 28 PHP
PHP pthreads v3在centos7平台下的安装与配置操作方法
Feb 21 PHP
PHP 对象接口简单实现方法示例
Apr 13 PHP
使用PHP和JavaScript判断请求是否来自微信内浏览器
Aug 18 #PHP
PHP的命令行命令使用指南
Aug 18 #PHP
使用PHP生成图片的缩略图的方法
Aug 18 #PHP
Nginx服务器上安装并配置PHPMyAdmin的教程
Aug 18 #PHP
详解PHP中的mb_detect_encoding函数使用方法
Aug 18 #PHP
深入讲解PHP Session及如何保持其不过期的方法
Aug 18 #PHP
php微信公众平台开发之获取用户基本信息
Aug 17 #PHP
You might like
PHP5 安装方法
2007/01/15 PHP
PHP英文字母大小写转换函数小结
2014/05/03 PHP
CI框架简单邮件发送类实例
2016/05/18 PHP
php中字符串和整数比较的操作方法
2019/06/06 PHP
php实现简易计算器
2020/08/28 PHP
WEB 浏览器兼容 推荐收藏
2010/05/14 Javascript
jquery ajax学习笔记2 使用XMLHttpRequest对象的responseXML
2011/10/16 Javascript
用js实现trim()的解决办法
2013/04/16 Javascript
jQuery学习笔记之jQuery原型属性和方法
2014/06/09 Javascript
JavaScript函数使用的基本教程
2015/06/04 Javascript
jQuery判断一个元素是否可见的方法
2015/06/05 Javascript
JS实现的5级联动Select下拉选择框实例
2015/08/17 Javascript
jQuery实现的自定义弹出层效果实例详解
2016/09/04 Javascript
Javascript获取background属性中url的值
2016/10/17 Javascript
js仿iphone秒表功能 计算平均数
2017/01/11 Javascript
深入探究AngularJs之$scope对象(作用域)
2017/07/20 Javascript
浅谈Vue下使用百度地图的简易方法
2018/03/23 Javascript
vue 表单输入格式化中文输入法异常问题
2018/05/30 Javascript
express+vue+mongodb+session 实现注册登录功能
2018/12/06 Javascript
微信小程序简单的canvas裁剪图片功能详解
2019/07/12 Javascript
基于 vue-skeleton-webpack-plugin 的骨架屏实战
2019/08/05 Javascript
extjs图表绘制之条形图实现方法分析
2020/03/06 Javascript
JS删除对象中某一属性案例详解
2020/09/08 Javascript
[45:25]OG vs EG 2019国际邀请赛淘汰赛 胜者组 BO3 第一场 8.22
2019/09/05 DOTA
python标准日志模块logging的使用方法
2013/11/01 Python
django开发教程之利用缓存文件进行页面缓存的方法
2017/11/10 Python
Python基于递归算法实现的汉诺塔与Fibonacci数列示例
2018/04/18 Python
手写一个python迭代器过程详解
2019/08/27 Python
python计算无向图节点度的实例代码
2019/11/22 Python
Pyqt5 关于流式布局和滚动条的综合使用示例代码
2020/03/24 Python
python爬虫分布式获取数据的实例方法
2020/11/26 Python
python基于pexpect库自动获取日志信息
2021/02/01 Python
canvas进阶之贝塞尔公式推导与物体跟随复杂曲线的轨迹运动
2018/01/10 HTML / CSS
校长竞聘演讲稿
2014/05/16 职场文书
投资意向协议书
2015/01/29 职场文书
如何打开Win11系统注册表编辑器?Win11注册表编辑器打开修复方法
2022/04/05 数码科技