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连接却还是同一个,造成运行混乱。