深入探究PHP的多进程编程方法
子进程的创建
一般的子进程的写法是:
<?php $pid=pcntl_fork(); if($pid==-1){ //创建失败 die('couldnotfork'); } 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($pidArras$key=>$pid) { if($myId==$pid)unset($pidArr[$key]); } }
然后使用crontab,来使此PHP程序每隔一段时间自动执行。
当然,示例代码比较简单,具体还需要考虑怎么防止多个子进程执行到同一条数据或者当前进程处理数据未完成时,crontab又开始执行PHP文件启用新的进程等等。
PHP多进程实现方式
下面来系统地整理一下PHP多进程的实现方式:
1.直接方式
pcntl_fork()创建一个进程,在父进程返回值是子进程的pid,在子进程返回值是0,-1表示创建进程失败。跟C非常相似。
测试脚本test.php
<?php //exampleofmultipleprocesses date_default_timezone_set('Asia/Chongqing'); echo"parentstart,pid",getmypid(),"\n"; beep(); for($i=0;$i<3;++$i){ $pid=pcntl_fork(); if($pid==-1){ die("cannotfork"); }elseif($pid>0){ echo"parentcontinue\n"; for($k=0;$k<2;++$k){ beep(); } }elseif($pid==0){ echo"childstart,pid",getmypid(),"\n"; for($j=0;$j<5;++$j){ beep(); } exit; } } //*** functionbeep(){ echogetmypid(),"\t",date('Y-m-dH:i:s',time()),"\n"; sleep(1); } ?>
用命令行运行
#php-ftest.php
输出结果
parentstart,pid1793 17932013-01-1415:04:17 parentcontinue 17932013-01-1415:04:18 childstart,pid1794 17942013-01-1415:04:18 17942013-01-1415:04:19 17932013-01-1415:04:19 17942013-01-1415:04:20 parentcontinue 17932013-01-1415:04:20 childstart,pid1795 17952013-01-1415:04:20 179317942013-01-1415:04:212013-01-1415:04:21 17952013-01-1415:04:21 17942013-01-1415:04:22 17952013-01-1415:04:22 parentcontinue 17932013-01-1415:04:22 childstart,pid1796 17962013-01-1415:04:22 17932013-01-1415:04:23 17962013-01-1415:04:23 17952013-01-1415:04:23 17952013-01-1415:04:24 17962013-01-1415:04:24 17962013-01-1415:04:25 17962013-01-1415:04:26
从中看到,创建了3个子进程,和父进程一起并行运行。其中有一行格式跟其他有些不同,
17931794 2013-01-1415:04:212013-01-1415:04:21
因为两个进程同时进行写操作,造成了冲突。
2.阻塞方式
用直接方式,父进程创建了子进程后,并没有等待子进程结束,而是继续运行。似乎这里看不到有什么问题。如果php脚本并不是运行完后自动结束,而是常驻内存的,就会造成子进程无法回收的问题。也就是僵尸进程。可以通过pcntl_wai()方法等待进程结束,然后回收已经结束的进程。
将测试脚本改成:
$pid=pcntl_fork(); if($pid==-1){ ... }elseif($pid>0){ echo"parentcontinue\n"; pcntl_wait($status); for($k=0;$k<2;++$k){ beep(); } }elseif($pid==0){ ... }
用命令行运行
#php-ftest.php
输出结果
parentstart,pid1807 18072013-01-1415:20:05 parentcontinue childstart,pid1808 18082013-01-1415:20:06 18082013-01-1415:20:07 18082013-01-1415:20:08 18082013-01-1415:20:09 18082013-01-1415:20:10 18072013-01-1415:20:11 18072013-01-1415:20:12 parentcontinue childstart,pid1809 18092013-01-1415:20:13 18092013-01-1415:20:14 18092013-01-1415:20:15 18092013-01-1415:20:16 18092013-01-1415:20:17 18072013-01-1415:20:18 18072013-01-1415:20:19 childstart,pid1810 18102013-01-1415:20:20 parentcontinue 18102013-01-1415:20:21 18102013-01-1415:20:22 18102013-01-1415:20:23 18102013-01-1415:20:24 18072013-01-1415:20:25 18072013-01-1415:20:26
父进程在pcntl_wait()将自己阻塞,等待子进程运行完了才接着运行。
3.非阻塞方式
阻塞方式失去了多进程的并行性。还有一种方法,既可以回收已经结束的子进程,又可以并行。这就是非阻塞的方式。
修改脚本:
<?php //exampleofmultipleprocesses date_default_timezone_set('Asia/Chongqing'); declare(ticks=1); pcntl_signal(SIGCHLD,"garbage"); echo"parentstart,pid",getmypid(),"\n"; beep(); for($i=0;$i<3;++$i){ $pid=pcntl_fork(); if($pid==-1){ die("cannotfork"); }elseif($pid>0){ echo"parentcontinue\n"; for($k=0;$k<2;++$k){ beep(); } }elseif($pid==0){ echo"childstart,pid",getmypid(),"\n"; for($j=0;$j<5;++$j){ beep(); } exit(0); } } //parent while(1){ //dosomethingelse sleep(5); } //*** functiongarbage($signal){ echo"signel$signalreceived\n"; while(($pid=pcntl_waitpid(-1,$status,WNOHANG))>0){ echo"\tchildendpid$pid,status$status\n"; } } functionbeep(){ echogetmypid(),"\t",date('Y-m-dH:i:s',time()),"\n"; sleep(1); } ?>
用命令行运行
#php-ftest.php&
输出结果
parentstart,pid2066 20662013-01-1416:45:34 parentcontinue 20662013-01-1416:45:35 childstart,pid2067 20672013-01-1416:45:35 206620672013-01-1416:45:362013-01-1416:45:36 20672013-01-1416:45:37 parentcontinue 20662013-01-1416:45:37 childstart,pid2068 20682013-01-1416:45:37 20672013-01-1416:45:38 20682013-01-1416:45:38 20662013-01-1416:45:38 parentcontinue 20662013-01-1416:45:40 childstart,pid2069 206920672013-01-1416:45:40 2013-01-1416:45:40 20682013-01-1416:45:40 20662013-01-1416:45:41 20692013-01-1416:45:41 20682013-01-1416:45:41 signel17received childendpid2067,status0 20692013-01-1416:45:42 20682013-01-1416:45:42 20692013-01-1416:45:43 signel17received childendpid2068,status0 20692013-01-1416:45:44 signel17received childendpid2069,status0
多个进程又并行运行了,而且运行大约10秒钟之后,用ps-ef|grepphp查看正在运行的进程,只有一个进程
lqling 2066 1388 016:45pts/1 00:00:00php-ft5.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($tasksas$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,checkoutresult foreach($tasksas$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"childstart,pid",getmypid(),"\r\n"; for($i=0;$i<5;++$i){ beep(); } exit(0); //*** functionbeep(){ echogetmypid(),"\t",date('Y-m-dH:i:s',time()),"\r\n"; sleep(1); } ?>
用命令行运行
#php-ftest1.php&
输出结果
task0[http://localhost/feedbowl/t2.php?job=task1]get: childstart,pid5804 58042013-01-1520:22:35 58042013-01-1520:22:36 58042013-01-1520:22:37 58042013-01-1520:22:38 58042013-01-1520:22:39 task1[http://localhost/feedbowl/t2.php?job=task2]get: childstart,pid5804 58042013-01-1520:22:35 58042013-01-1520:22:36 58042013-01-1520:22:37 58042013-01-1520:22:38 58042013-01-1520:22:39 task2[http://localhost/feedbowl/t2.php?job=task3]get: childstart,pid5804 58042013-01-1520:22:35 58042013-01-1520:22:36 58042013-01-1520:22:37 58042013-01-1520:22:38 58042013-01-1520:22:39
从打印的时间看到,多个任务几乎是同时运行的。