1.关于多进程的基础知识:http://www.cnblogs.com/Anker/p/3271773.html
2.php变成的三种方法:http://www.jb51.net/article/71232.htm
子进程的创建
一般的子进程的写法是:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
<?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循环就可以了。
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
|
#如果获得的总数小于或等于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
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
|
<?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);
}
?>
|
用命令行运行
1
|
#php -f test.php
|
输出结果
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
|
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()方法等待进程结束,然后回收已经结束的进程。
将测试脚本改成:
1
2
3
4
5
6
7
8
9
10
11
12
|
$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){
...
}
|
用命令行运行
1
|
#php -f test.php
|
输出结果
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
|
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. 非阻塞方式
阻塞方式失去了多进程的并行性。还有一种方法,既可以回收已经结束的子进程,又可以并行。这就是非阻塞的方式。
修改脚本:
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
|
<?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);
}
?>
|
用命令行运行
1
|
#php -f test.php &
|
输出结果
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
|
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
是父进程,子进程被回收了。
子进程退出状态
1
|
pcntl_waitpid(-1,
$status
, WNOHANG)
$status
|
返回子进程的结束状态
windows下多线程
windows系统不支持pcntl函数,幸好有curl_multi_exec()这个工具,利用内部的多线程,访问多个链接,每个链接可以作为一个任务。
编写脚本 test1.php
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
|
<?php
date_default_timezone_set(
'Asia/Chongqing'
);
$tasks
=
array
(
);
$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
1
2
3
4
5
6
7
8
9
10
11
12
13
|
<?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);
}
?>
|
用命令行运行
1
|
#php -f test1.php &
|
输出结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
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
|
从打印的时间看到,多个任务几乎是同时运行的。
1.for循环创建子进程的时候一定记住子进程要exit,否则,子进程也会参与到循环创建子进程的过程中,就乱了。
2.不要在子进程中使用父进程的实例,之前百度金融雷达踩坑。
3.孤儿进程,父进程退出,子进程还在跑。这个没事,一般不用处理
4.僵尸进程,子进程退出,父进程还在跑,但是没回收子进程。因此,父进程里一定要有回收子进程的逻辑。如非阻塞方式里面的garbage函数进行了处理,实际上garbage就干了一件事最关键就是:pcntl_waitpid函数,只有执行这个函数,这个子进程才最终释放。
5.pcntl_waitpid函数,挂起来父进程知道要等待的那个子进程运行完毕。WNOHANG的作用是如果没子进程退出就立刻返回0,不等待,加了这个参数后,这个函数所在的父进程不会阻塞,即不会一直等到所有子进程结束后才结束。如果有等待所有子进程结束后父进程才结束,可以在父进程中记录下所有fork出去的子进程,然后循环执行这个函数pcntl_waitpid(-1,$status),没返回一个子进程就代表这个子进程结束了,记录下这个子进程,直到和父进程之前记录下的所有fork出去的进程数量一致了再结束父进程。
<?php $test = array(1,2,3); //declare(ticks=1); foreach($test as $v) { pcntl_signal(SIGCHLD,'a'); pcntl_signal(SIGINT,'signal_handler');//父子进程都会被注册这个信号处理器 pcntl_signal_dispatch();// 如果你在这段语句执行过以后你传进来信号,则信号不会被处理 $pid = pcntl_fork(); if($pid == -1) {die('could not fork');} else { if($pid) { sleep(10); pcntl_wait($status);var_dump($status); var_dump('i am dad' . $pid); } else { var_dump('i am kid' . $pid); sleep(10);//如果信号在这个函数执行的过程中被输入,这个过程会被打断。直接执行下一个函数'i am kid end',并会执行dispatch之前的所有函数。即接收信号会打断接收时正在执行的函数。 var_dump('i am kid end');exit; } }var_dump('-------',$pid); // pcntl_signal_dispatch();// 如果你放在这里的话,在这个函数执行之前输入的信号,信号会被执行,但是前面的代码都会被执行就算你在
}function a() {echo 'handle child';}function signal_handler() {'i am kid'这句话之前输入了中断信号,程序也不会中断,还是会打印出来这句'i am kid'.
);