探索 PHP 多进程
鉴于上两篇文章,有些同学对php的多进程表现出很大的兴趣,所以我又要来献丑了,感觉不错就点赞,感觉有帮助就打赏,反正要鼓励一下宝宝,哈哈哈。
开始探索
进程是系统进行资源分配和调度的基本单位,执行一个php程序,默认只会生成一个进程,当该进程发生请求IO时,就会被挂起,让出CPU,php的多进程编程的目的就是要在多个进程并发的执行任务,当其中一些进程由于发生IO被挂起时,还有一些进程可以利用CPU执行任务,这样就可以充分利用系统资源来完成我们的大批量任务了(特别适合IO型任务)。php多进程开发,主要有两条路:
- 第一就是php内置的pcntl扩展(依赖linux系统),
- 第二就是比较流行的扩展swoole下的多进程。
以下我们来实际操作一波!
1. pcntl_fork
不多说了,直接上代码:
$pid = pcntl_fork(); //fork一个子进程
if ($pid > 0) { //父进程会获取到fork到的子进程的进程id
dump('我是父进程');
} elseif ($pid == 0) { //$pid=0时为刚刚派生出的子进程
dump('我是子进程');
} else {
dump('创建子进程失败');
}
执行结果如下,证明创建成功:
就那么简单吗?不是的。还有很多关键进程创建和使用过程要注意的点,以下再来深入探索一下:
- 孤儿进程
孤儿进程已经很白话了,创建它的父进程死掉了,但是它自己还没死,还在运行,但是linux宪法规定,凡是父进程死了还在运行的子进程都要认进程号为1的进程做父亲,这样的话,子进程就是可以脱离ssh,直接进程后台运行模式,由干爹进程1来管理了。利用这一点,可以开发守护进程。
$pid = pcntl_fork();
if ($pid > 0) {
$seconds = 5;
while ($seconds--) {
dump('我是父进程,我的寿命只有5秒,剩余:' . $seconds);
sleep(1);
}
dump('我是父进程');
} elseif ($pid == 0) {
$seconds = 10;
while ($seconds--) {
dump('我是子进程,我的寿命有10秒,剩余::' . $seconds);
sleep(1);
}
} else {
dump('创建子进程失败');
}
执行结果如下
前5秒,父子进程协同运行,保证正常的父子关系
systemd,1 --switched-root --system --deserialize 22
├─sshd,1090 -D -u0
│ ├─sshd,3711
│ │ └─sshd,3713
│ │ └─bash,3714
│ │ └─sudo,3741 -i
│ │ └─bash,3742
│ │ └─php,11178 think debug //父进程
│ │ └─php,11179 think debug //子进程
后5秒,父进程死掉了,子进程退出ssh,认了进程号为1的进程做干爹,继续执行5秒,执行完毕被进程1收回,如果一直执行就成为守护进程了。
systemd,1 --switched-root --system --deserialize 22
├─php,11179 think debug
- 僵尸进程
僵尸进程就是已经执行完了,exit了,但是父进程没有主动回收资源,父子进程关系没有变化,所以依然存在于进程表中。由于僵尸进程会占用系统资源,所以是有危害的,应该避免。怎么模拟产生呢,很简单,将上面的代码变一下,使父进程长命于子进程,如下:
$pid = pcntl_fork();
if ($pid > 0) {
$seconds = 10;
while ($seconds--) {
dump('我是父进程,我的寿命只有10秒,剩余:' . $seconds);
sleep(1);
}
dump('我是父进程');
} elseif ($pid == 0) {
$seconds = 5;
while ($seconds--) {
dump('我是子进程,我的寿命有5秒,剩余::' . $seconds);
sleep(1);
}
} else {
dump('创建子进程失败');
}
}
执行结果如下:
前5秒,父子进程协同运行,保证正常的父子关系
systemd,1 --switched-root --system --deserialize 22
├─sshd,1090 -D -u0
│ ├─sshd,3711
│ │ └─sshd,3713
│ │ └─bash,3714
│ │ └─sudo,3741 -i
│ │ └─bash,3742
│ │ └─php,11204 think debug
│ │ └─php,11205 think debug
后5秒,子进程死掉了,父进正常运行,但是子进程还存在于进程表中,占用系统资源,直到父进程也死掉才会一起被释放。
├─sshd,1090 -D -u0
│ ├─sshd,3711
│ │ └─sshd,3713
│ │ └─bash,3714
│ │ └─sudo,3741 -i
│ │ └─bash,3742
│ │ └─php,11204 think debug
│ │ └─(php,11205) //僵尸进程
- 避免僵尸进程和孤儿进程
想要避免出现僵尸进程和孤儿进程,主要是保证两点:一是,父进程不能早死于子进程;二是,父进程能及时回收运行结束的子进程。这方面,主要使用php提供了两个函数来解决:pcntl_wait、pcntl_waitpid。编辑代码如下:
$pid = pcntl_fork();
if ($pid) {
dump('这里是父进程');
pcntl_wait($status); //主进程会挂起,等待第一个子进程结束
//等同于, pcntl_waitpid($pid,$status);
sleep(10); //模拟干活
} elseif ($pid == 0) {
sleep(20); //模拟干活
dump('这里是子进程');
} else {
dump('创建子进程异常');
}
运行上面代码,发现僵尸进程和孤儿进程问题解决了,因为主进程始终保持等待和回收子进程,但是这样之后,任务执行变成了串行的、同步的,根本实现不了父子进程同时干活的目的。
- 多进程异步非阻塞工作
毫不犹豫,怼代码:
$pids = [];
for ($i = 0; $i < 20; $i++) {
$pid = pcntl_fork();
if ($pid) {
$pids[] = $pid;
} elseif ($pid == 0) {
sleep(20);
dump('这里是子进程');
exit();//执行完,一定要结束,不然就会走进创建子进程的死循环
} else {
dump('创建子进程异常');
}
}
//等待回收所有子进程
foreach ($pids as $pid) {
pcntl_waitpid($pid, $status);
//第三个参数有两个可能值:WNOHANG-回收子进程时异步不挂起主进程
// WUNTRACED-子进程已经退出并且其状态未报告时返回
}
执行结果如下,整个过程20秒结束
├─sshd,1073 -D -u0
│ ├─sshd,3133
│ │ └─sshd,3135
│ │ └─bash,3136
│ │ └─sudo,3163 -i
│ │ └─bash,3164
│ │ └─php,3361 think debug
│ │ ├─php,3363 think debug
│ │ ├─php,3364 think debug
│ │ ├─php,3365 think debug
│ │ ├─php,3366 think debug
│ │ ├─php,3367 think debug
│ │ ├─php,3368 think debug
│ │ ├─php,3369 think debug
│ │ ├─php,3370 think debug
│ │ ├─php,3371 think debug
│ │ ├─php,3372 think debug
│ │ ├─php,3373 think debug
│ │ ├─php,3374 think debug
│ │ ├─php,3375 think debug
│ │ ├─php,3376 think debug
│ │ ├─php,3377 think debug
│ │ ├─php,3378 think debug
│ │ ├─php,3379 think debug
│ │ ├─php,3380 think debug
│ │ ├─php,3381 think debug
│ │ └─php,3382 think debug
这里的代码设计,主进程负责创建、回收子进程,子进程负责实际的干活,整个过程都是异步的,所以执行效率非常高。
- 进程间通信-共享内存
php进程间的共享内存函数有两大类,简介如下:
I:首先简单介绍一下shmop_xxx类
相关函数,使用起来也简单,不做一一解释了(偷懒)
编辑代码如下
$key = ftok(__FILE__, 'c'); //ipc标识
$shmid = shmop_open($key, 'c', 0655, 1024);//申请1024字节的内存空间
$pid = pcntl_fork();
if ($pid > 0) {
sleep(2);//睡眠一下,等待子进程写数据进内存
$info = shmop_read($shmid, 0, 100);//读取内存数据
dump('我是父进程,从共享进程中读取到的信息是:' . $info);
pcntl_wait($status); //回收子进程
} elseif ($pid == 0) {
//从申请的共享内存的第0个字节开始写数据
$size = shmop_write($shmid, "hello father,i'm your son!", 0);
dump('我是子进程,我向内存里写入字节数是:' . $size);
exit();
} else {
dump('创建子进程失败');
}
执行结果如下,父子进程能够通过通过共享内存实现信息交流
II:再来介绍一下shm_xxx类,需要安装Semaphore 扩展
相关函数,使用起来也简单,不做一一解释了(再次偷懒)
编辑代码如下:
$key = ftok(__FILE__, 'c'); //ipc标识
$shmid = shm_attach($key, 1024, 0655);
$variable_key = 1; //类似设置key-value的key,不过这里要求是int
$pid = pcntl_fork();
if ($pid > 0) {
sleep(2);//睡眠一下,等待子进程写数据进内存
$info = shm_get_var($shmid, $variable_key);//获取对应key的值
dump('我是父进程,从共享进程中读取到的信息是:' . $info);
pcntl_wait($status); //回收子进程
} elseif ($pid == 0) {
//对key设置值
$size = shm_put_var($shmid, $variable_key, "hello father,i'm your son!");
dump('我是子进程,我向内存里写入字节数是:' . $size);
exit();
} else {
dump('创建子进程失败');
}
shm_remove($shmid); // 从系统中移除
shm_detach($shmid); //关闭和共享内存的连接
III:除了上面的方式之外,还可以使用Redis、memcache等方式实现进程间的数据共享
- 进程间通信-信号量
信号量相关函数如下,如需细究,请自行研究:
以下我怼一下自己粗狂的代码:
$key = ftok(__FILE__, 'c'); //ipc标识
$signal_id = sem_get($key); //获取信号id
$pids = [];
for ($i = 0; $i < 5; $i++) {
$pid = pcntl_fork();
if ($pid > 0) {
$pids[] = $pid;
sem_acquire($signal_id); //获取信号量(阻塞式)
$datetime = date('Y-m-d H:i:s');
dump("我是父进程,正在创建进程:{$i},创建时间:{$datetime}");
sem_release($signal_id);//释放信号量
} elseif ($pid == 0) {
sem_acquire($signal_id); //获取信号量(阻塞式)
sleep(2);
sem_release($signal_id);//释放信号量
exit();
} else {
dump('创建子进程失败');
}
sem_remove($signal_id);//删除信号id
//等待回收所有子进程
foreach ($pids as $pid) {
pcntl_waitpid($pid, $status);
}
}
执行结果如下:
可以发现,父进程不是一下子生成5个子进程的,因为子进程做了信号量控制,当某个进程获得信号量时,其他进程处于等待信号量时间,只有拥有信号量的进程释放信号量时,其他进程才能获得,所以上面父进程每次创建子进程后,要等子进程睡两秒,然后再继续创建下去,如此类推。
2. swoole多进程
swoole的官网学习文档,wiki.swoole.com/wiki/page/p-proces...
总结
进程是十分昂贵的资源,php每个进程占用内存从几M到几百M不等,过多的创建进程反而会严重拖垮系统,使系统变得缓慢,甚至宕机。php进程都是独立的运行单位,进程间的数据库、Redis连接都是独立的,要做好资源使用控制和进程间通信。php多进程编程适用于cli模式,对于php-fpm运行模式下,尽量不要采取多进程编程,会出现无法预测的问题。
本作品采用《CC 协议》,转载必须注明作者和本文链接
推荐文章: