PHP能否支持多进程?
在日常开发中,我们用到最多的php多进程场景莫过于就是使用php-fpm了,php-fpm作为php的多进程管理器,当我们使用nginx作为webserver时,来自用户的请求会根据nginx的路由配置把以php为后缀的文件转发给我们的php-fpm,而这里的php-fpm就是一个多进程管理器,多个用户的同时请求api,那么php-fpm则会开启多个php的处理进程进行处理。说了php-fpm,我们使用php的多进程方式还有swoole,pcntl_fork等,而这里我讲的主要是php在写服务器脚本使用多线程的情况下,首先要使用pcntl_fork函数,我们必须先检查php是否支持该扩展,使用php -m 查看php所有扩展。如果有pcntl则说明是安装了该多进程扩展,可以使用多进程功能,如果没有,则需要自行去安装该扩展。
二,关于pcntl_fork的介绍
当我们调用pcntl_fork函数时,成功时,在父进程执行线程内返回产生的子进程的PID,在子进程执行线程内返回0。失败时,在 父进程上下文返回-1,不会创建子进程,并且会引发一个PHP错误。是这样的,我们调用函数创建进程的时候,函数执行时有时间的,而新的进程刚好是在函数执行开始和结束之间创建出来的,这样,新的进程也执行了这个函数,所以函数也需要有返回值。那么对于该函数一次执行之后,父进程和子进程都会受到该函数的返回值,由于父进程创建了子进程,而子进程并没有创建新的进程,所以子进程对于这个函数的返回结果是没有的,所以就给他赋了一个0。而父进程创建了子进程,子进程是存在pid的,所以就得到了那个进程的pid。
所以综上我们可以发现调用pcntl_fork可能返回的值有三个,分别是-1,0,N,所以我们在使用pcnt_fork时需要分别对三个返回值做对应的判断,当返回值是-1的时候则说明创建子进程失败,当返回值是0的时候说明正是子进程,这是我们则可以在if分支下处理需要子进程执行的任务,当返回值是一N(这里N>0),则说明这是父进程的分支。
三,避免僵尸进程之pcntl_waitpid的使用
何为僵尸进程,就是存在一个进程已经执行完任务,但是该进程未被系统和父进程回收的情况下,那么该进程就是一个存在系统当中,不会做任何事,且一直存在的进程,那么该进程就是一个僵尸进程,在这里,有一种情况是,父进程创建子进程后,父进程比子进程先做完任务然后终止,那么子进程在做完任务后无法被父进程处理,子进程就会变为僵尸进程,这个时候则需要使用pcntl_waitpid的函数来避免这种情况。这个函数的功能的是,当父进程执行这个函数,那么父进程就会一直处于等待状态下,直到子进程完成任务,父进程回收子进程。所以我们在进行php的多进程脚本编程的时候需要父进程执行该函数,用于避免父进程早早退出,无法回收子进程的情况。
四:实战环节
这周接到一个需求,当商品状态发生变化,需要给关注该商品的用户推送一条短信。
需求分析:
1,当商品发生变化,查询关注该商品的用户id,并且把该uid存入redis集合。
2,使用crontab每天晚上九点半,执行脚本对用户推送短信。
考虑到若该商品关注的用户量大数量级在10万个用户,那么我们用单进程的方式推送短信,在php的同步阻塞模式下,单进程时非常耗费执行时间的。所以我们这里要考虑用多进程的方式,开启10个进程分别对10万个用户进行推送,那么在原有的时间耗费上,我们可以提高10倍的效率。
程序思路:10个进程处理10万条推送,那么单个进程一次需要处理1万条推送。
- 首先我们需要拿到redis的集合的总量,这里假设总量是10万。
- 然后我们用总量除以进程数,ceil(总量/进程数量),这里用到了ceil函数,不了解的可以搜索该函数功能
- 然后,for循环开启十个进程。
- 然后,单个进程我传入参数上面相除得到的值,在单个进程里面执行这个次数。
发送短信代码片段:
/**
* 该function用于处理促销redis集合.
*
* @param integer $time 多进程下单次循环次数.
*
* @return void
*/
public function managePromotionRedis($time)
{
$today = date("Y-m-d");
$cacheName = $today.'promotion';
$count = 0;
while ($count <= $time) {
$uid = \Module_Base::Instance()->mRedis()->sPop($cacheName);
if (empty($uid)) {
$this->log->writeLine($cacheName.'队列已经空'.PHP_EOL);
return;
}
$pid = getmypid();
$this->sendMsgToUid($uid,1);
$this->log->writeLine($cacheName.'队列出了'.$uid.PHP_EOL);
echo $cacheName.'队列出了'."$pid".'这个uid是'.$uid.PHP_EOL;
$count++;
}
return;
}
这里我们传入一个参数,$time代表的是一个进程单次推送短信次数
开启十个进程片段:
/**
* 该function用于多进程处理消息推送.
*
* @return void
*/
public function process()
{
if (function_exists('pcntl_fork')) {
$pids = array();
for ($x = 0; $x < $this->processNumber; $x++) {
$pid = pcntl_fork();
if ($pid == -1) {
$this->log->writeLine("创建子进程失败!!\n");
} elseif ($pid) {
// 父进程.
echo '父进程最后执行'.PHP_EOL;
$pids[] = $pid;
} else {
// 子进程.
$this->run($this->processNumber);
echo '测试'.PHP_EOL;
exit();
}
}
// 等待子进程结束
foreach ($pids as $pid) {
pcntl_waitpid($pid, $status);
}
} else {
echo '当前PHP版本不支持pcntl_fork功能,运行失败'.PHP_EOL;
exit();
}
}
/**
* 该function用于同时执行上新和促销redis集合.
*
* @param integer $processNumber 进程数量.
*
* @return void
*/
public function run($processNumber)
{
try {
$today = date("Y-m-d");
$promotionCount = \Module_Base::Instance()->mRedis()->SCARD($today.'promotion');
$newProjectCount = \Module_Base::Instance()->mRedis()->SCARD($today.'newProduct');
$SinglePromotionRunTime = ceil($promotionCount / $processNumber);
$SingleNewProductTime = ceil($newProjectCount / $processNumber);
$this->manageNewProductRedis($SingleNewProductTime);
$this->managePromotionRedis($SinglePromotionRunTime);
} catch (Exception $exception) {
$this->log->writeLine($exception->getMessage());
exit();
}
}