首先说明一下我的环境是 Nginx + spawn-cgi。spawn-cgi启动了64个php-cgi,端口为9000(./spawn-cgi -p 9000 -F 64 -f ~/php-cgi)
接下来假如我们有这样一个需求:每次用户输入一串数据,我们将用户的信息以及这串数据记录到文本当中。程序如下:
test.php
<?php
$usrinfo = isset($_GET["usrinfo"])?$_GET["usrinfo"]:exit(1);
$stinfo = isset($_GET["stinfo"])?$_GET["stinfo"]:exit(1);
echo $stinfo;
$pid = posix_getpid();
$fp = fopen("usrinfo.txt","a+");
fwrite($fp,"user:".$usrinfo." stinfo:".$stinfo."--".$pid."\n");
fclose($fp);
运 行ab命令:ab -n 1000 -c 100 "http://localhost:9080/test.php"(ab是apache自带的网络测 试工具-n 1000表示共发起1000次请求,-c 100表示模拟每次100个请求),查看usrinfo.txt文件结果,文件输出 -
user:usr_test stinfo:st_test--22311--47947
user:usr_test stinfo:st_test--22346--2782
user:usr_test stinfo:st_test--22285--55237
user:usr_test stinfo:st_test--22305--63849
user:usr_test stinfo:st_test--22288--9748
user:usr_test stinfo:st_test--22296--37314
...................................................
....................................................
user:usr_test stinfo:st_test--22311--37033
......................................................
-------------------------------------------
user:usr_test stinfo:st_test--22346--16242
........................................................
/* 以上结果共1000行 */
由 于我们的ab测试发起1000次请求,所以最终结果是1000行,并且fastcgi维护的是一个装有php-cgi的进程池,使php-cgi解析了一 次php程序作业以后不自动销毁而是再次解析其他的php程序作业。因此有如图所示的22311号进程处理完毕一个作业以后再次处理其他作业。
通 过结果我们看到,我们程序输出的数据是正确的。而没有出现由于fastcgi并发执行而导致的写操作乱序现象。事实上,PHP的fwrite函数调用了C 的fwrite函数(缓冲写),C语言的fwrite函数调用了Linux系统调用write,而无论fwrite或者write函数都是原子操作,是不 可分割的,所以上述的代码段可以正常运行。
如果我们将一次fwrite调用修改成两次,代码如下:
<?php
$usrinfo = isset($_GET["usrinfo"])?$_GET["usrinfo"]:exit(1);
$stinfo = isset($_GET["stinfo"])?$_GET["stinfo"]:exit(1);
echo $stinfo;
$pid = posix_getpid();
$fp = fopen("usrinfo.txt","a+");
$num = rand(0,100000);
fwrite($fp,"user:".$usrinfo." stinfo:".$stinfo."--".$pid."--".$num."\n");
fwrite($fp,"talking 1 -- pid:$pid and num:$num\n");
fclose($fp);
再次运行 ab -n 1000 -c 100 "http://localhost:9080/test.php",则再次打开usrinfo.txt
user:usr_test stinfo:st_test--22319--96796
user:usr_test stinfo:st_test--22303--79356
talking 1 -- pid:22319 and num:96796
talking 1 -- pid:22303 and num:79356
user:usr_test stinfo:st_test--22293--23359
user:usr_test stinfo:st_test--22309--48593
talking 1 -- pid:22293 and num:23359
talking 1 -- pid:22309 and num:48593
user:usr_test stinfo:st_test--22338--22198
user:usr_test stinfo:st_test--22316--75875
talking 1 -- pid:22338 and num:22198
talking 1 -- pid:22316 and num:75875
..................................................................................................
我 们看到,程序的写顺序已经被完全破坏,如果我们把spawn-cgi开启php-cgi的数减少到1(./spawn- cgi -p 9000 -F 1 -f ~/php-cgi),则该程序结果运行正常,而如果这样,php-cgi就变成了单进程了,而-cgi开启的 php-cgi越多,该程序的结果就会越混乱。
其实,很多时候,我们并没有考虑我们php代码的并行能力,尤其是在我们的php代 码对某个资源可读可写的时候。但这并不是说php的所有操作就都是原子的,事务的,可并行的。由于我们的php脚本是运行在fastcgi容器中,而 fastcgi是多进程的,所以如果php程序访问了临界资源,势必造成程序结果的不正确性。
解决问题的办法是使用锁机制。php没有继承posix标准支持的unix锁:比如记录锁fcntl,线程锁等,而只封装了一个linux系统调用flock,flock形式为flock($fp,$type),其中$fp为文件句柄,而$type为
/* 当一个文件的打开方式是可读可写的,通常需要向文件加入锁机制 */
1. LOCK_SH 共享锁:
通常为进程向文件请求读操作时需加共享锁。共享锁可支持任意个进程间的读操作,如果写一个加了共享锁的文件则进程阻塞进入SLEEP状态值到共享锁解锁
2. LOCK_EX 独占锁:
通常为进程向文件的写操作加独占锁,一旦文件加上了该锁,则其他任意进程访问该文件时都会阻塞,直到解锁为止。
3. LOCK_UN 解锁
为加锁的文件句柄解锁
这样的加锁方式必然可以保证加锁程序块的原子性,但同时也牺牲了程序的效率,因此,我们实际的程序中应该在程序的加锁和解锁代码间嵌入尽量少的程序逻辑(尤其是独占锁),保证程序尽快解锁。
最后,附上加上锁机制以后的程序:
<?php
$usrinfo = isset($_GET["usrinfo"])?$_GET["usrinfo"]:exit(1);
$stinfo = isset($_GET["stinfo"])?$_GET["stinfo"]:exit(1);
echo $stinfo;
$pid = posix_getpid();
$fp = fopen("usrinfo.txt","a+");
$num = rand(0,100000);
flock($fp,LOCK_EX);
fwrite($fp,"user:".$usrinfo." stinfo:".$stinfo."--".$pid."--".$num."\n");
fwrite($fp,"talking 1 -- pid:$pid and num:$num\n");
flock($fp,LOCK_UN);
fclose($fp);
运行该程序,产生正确的结果。
user:usr_test stinfo:st_test--22301--63987
talking 1 -- pid:22301 and num:63987
user:usr_test stinfo:st_test--22323--81895
talking 1 -- pid:22323 and num:81895
user:usr_test stinfo:st_test--22302--66557
talking 1 -- pid:22302 and num:66557
user:usr_test stinfo:st_test--22282--82967
talking 1 -- pid:22282 and num:82967
user:usr_test stinfo:st_test--22333--6534
talking 1 -- pid:22333 and num:6534