PHP中的PCNTL可以实现多进程编程,由于项目场景需要,试用了一下,感触颇多,也长了不少见识,就此对遇到的问题小做一个总结,以备不时之需。
问题一:fork泛滥
我想在一个父进程中起10个子程来完成我的工作,代码如下:
for($i = 0; $i < 10; $i++) {
$pid = pcntl_fork();
if($pid == -1) {
echo "Could not fork!\n";
exit(1);
}
if(!$pid) {
//child process workspace
//TODO
Api::refreshCache();
}
}
由于Api::refreshCache逻辑中有数据库操作,代码一执行,就把DB搞死了,报了N多个too many connections。我以为是DB原本就抽风了,检查DB,正常。
最终发现,这一段代码fork的可不是10个子进程,而是
20 + 21 + 22+ 23 + 24 + ... + 29 = 210-1 = 1023 个子进程。
恐怖了吧?这是因为子进程又fork子进程,并且子进程不共享父进程$i变量更新的值,由此导致数量成指数关系增长。
为了避免这个问题,代码修改如下:
for($i = 0; $i < 10; $i++) {
$pid = pcntl_fork();
if($pid == -1) {
echo "Could not fork!\n";
exit(1);
}
if(!$pid) {
//child process workspace
//TODO
Api::refreshCache();
exit; //子进程逻辑执行完后,马上退出,以免往下走再fork子进程,不好控制
}
}
只要在子进程逻辑执行完后,加一个exit,一切都在撑握中了。这样,只有一个父进程,父进程后续对子进程的管理也会清晰很多。
问题二:单例模式下DB连接被虐
/*
* 前面或远或近的地方,已经有一个$db = &MySql::getInstance();了
*/
for($i = 0; $i < 10; $i++) {
$pid = pcntl_fork();
if($pid == -1) {
echo "Could not fork!\n";
exit(1);
}
if(!$pid) {
//child process workspace
//TODO
$db = &MySql::getInstance();
$sql = "XXX";
$result = $db->getAll($sql);
exit;
}
}
这段代码,十有八九都会报mysql has gone away.或者其它一些数据fetch方面的错误。究其原因,是因为各个子进程创建时,就已经继承了父进程一份完全一样的拷贝。对象可以拷贝,但是已创建的连接可不能拷贝成多个,由此产生的结果,就是各个进程都使用同一个mysql连接,各干各的事,最终产生莫名其妙的冲突。
解决办法:
我们显然不可能完全保证在fork进程之前,父进程不会创建mysql连接实例,因此,解决方案只能靠子进程本身了。可以想象,我们只需要在子进程中获取的实例只与当前进程相关,这个问题就不存在了。解决办法就是稍微改造一下mysql类实例化的静态方式,与当前进程ID绑定。
public static function &getInstance() {
static $instances = array();
$key = getmypid(); //获取当前进程ID
if(empty($instances[$key])) {
//实例化mysql类动作
$classname = __CLASS__;
$instances[$key] = new $classname();
}
return $instances[$key];
}
总结:子进程创建时,拷贝了父进程当时拥有的所有资源,虽说后面对资源的修改互不影响,但DB连接却还是同一个,造成运行混乱。