PHP工艺研究
将PHP作为解释器运行是通过线程或进程实现的。 如果使用Apache,则可能使用多线程模型。 使用php-fpm意味着使用多进程模型。 这里用多进程模型进行说明)。 服务器每次收到请求时都启动PHP进程。 每个PHP进程平均消耗2米左右的内存。 (默认值最大为8米,可以设定参数。 独立的过程使PHP可以专注于自己的解释工作,程序员也可以从复杂的代码逻辑中走出来,无需担心资源冲突和各种锁定问题。 独立进程很好,但这要多进程或异步高速化的成本非常高(主要是开发难度)。 如果必须通过PHP实现多进程和异步,其实很容易做到。
PHP有很多第三方扩展。 例如,Swoole可以像节点一样异步PHP。 PHP官方扩展库pcntl_*可以轻松实现多进程。 虽然扩张很好,但是在实用上必须慎重。 方便的同时也带来风险。 例如,在多进程控制中,如果处理不好,程序容易发生死锁,CPU内存死机,服务器宕机。 异步回调的编码方式与PHP自身的编程思想有一定的差异,做不好也是灾难。
当然不是说太可怕,但是在实际的项目中,有很多必须考虑多进程和异步优化程序的情况。 在这里,列举了邮件和邮件等一般的例子“发送消息通知”。 在此,说明实际的方案。 企业需要向200瓦的用户发送邮件通知。 消息界面最多支持100次/秒的调用频率,而消息界面每次调用需要300毫秒。 如果在单进程中运行脚本,则发送邮件需要7天。 如果我们启动30个过程,每秒可以发送100封邮件,6个小时内可以发送,速度可以加快30倍。 优化方案确定后,让我们来看看如何在PHP中实现这样的脚本。
一、pcntl扩展入门;
有关使用pcntl扩展创建多进程的详细信息,请参见以下代码
函数演示(阵列$电话列表) $ CNT=计数) $电话列表); //测试数组大小
$slice=30; //需要调用的进程数
$ master=array _ chunk ($电话列表,地址) $ CNT/$片断);
$儿童列表=[ ]; wile($slice=0) )。
{
$ PID=pcntl _福克(; if($PID0) {
$儿童列表[ $ PID ]=1; //$pid0表示当前正在运行父进程的代码
//在这里什么也不做比较好。 每次执行pcntl_fork时,都会执行这里的代码。
//这里的代码执行结束后,将$pid设定为0,jump到pcntl_fork代码后,重新进行判断
}elseif($PID==0) )//在此写下我们的逻辑
福克斯($ master [ $ slice ] as $ val
//这里发生邮件
echo sprintf (' % schild : % srn ',$slice,$val );
//子进程运行结束后,必须关闭;
exit (;
}else
//即使程序发生错误,也需要关闭程序
exit (;
}
$slice----;
//等待所有子进程结束后再回收资源
while (! 空白($儿童列表) )
$子儿童身份证=pcntl _ wait ($状态); if ($儿童id0) {成套} $儿童列表[ $儿童id ] };
}
}
(/**运行结果如下,phone不连续
Slice id:19、手机:66558
Slice id:23、手机:79921
Slice id:19、手机:66559
Slice id:23、手机:79922
Slice id:19、手机:66560
Slice id:23、手机:79923
Slice id:19、手机:66561
Slice id:23、手机:79924
Slice id:19、手机:66562
Slice id:23、手机:79925
*/
通过pcntl扩展,一些代码使用多进程将消息通知功能提高了30倍。 但是,明明是这么简单的多进程编码,为什么我开始用文章来表达这么复杂的事情呢?
难点还是进程间通信。 这是因为向用户发送邮件的各个子进程相对独立,进程之间没有通信,不相互传递数据的状态。 不会发生资源断开和锁定的问题。 如果需求发生变化,就需要按照用户的活跃度从高到低的顺序给用户发送邮件。 我该怎么办?
简单地说,一个盘子里有30个苹果,所以需要发给30人,3人有分发苹果的责任。 最简单的方法是先把苹果分成三份,三个人各分一份,这样可以马上发货。 但是,如果要按照苹果的大小顺序发,先发大苹果,这个时候我们不能分成三份,只能三个人互相挣现在最大的,很容易就吵起来。 我该怎么办? 最常见的方法是用一个工具把所有的苹果
按由大到下的顺序放在里面,每次只能取一个,这样就解决了资源抢占的问题。关于进程间资源抢占的问题非常的复杂,编码难度非常高,这也是为什么很少使用PHP跑多进程的原因。当需要用到多进程时我们更愿意去使用Python或者Java,它们对多线程封装的更好。需要重点说的是PHP并不是不能写多进程的程序,也不是像其他人说的不稳定,而是编码费时,维护成本高。
二. 进程间通信
常见的进程通信方式有:消息队列、共享内存与信号量、管道、socket,我将一一举例说明。
消息队列
『消息队列』是在消息的传输过程中保存消息的容器。消息队列管理器相当于消息发送者和接收者的中介。消息队列的主要目的是创建路由并且保证消息可靠传递;如果发送消息时接收者不可用,消息队列会保留消息,直到有人接收它。
消息队列可提供临时存储的功能并且能保证消息可靠的传递,我们正好使用它实现进程间通信。当然消息队列不单单用于进程间通信,他的应用领域非常广。比如消息队列非常适用于解决消费者和生产者的问题,因为生产者和消费者之间总会存在『速度差』。比如生产者突然少了10个,两边处理的速度就会不平衡,会导致排队阻塞,服务不可用。这肯定不是我们想看到的,如果这时候引入消息队列将两个系统解耦,无论谁慢了都不会影响整体业务。
function demo(array $phoneList){ global $msgQueue;$cnt = count($phoneList); //测试数组大小
$slice = 3; //需要调用的进程数量
$childList = []; //主进程先发送一条消息,告诉子进程可以发送第一条短信了
msg_send($msgQueue,MSG_TYPE,0); while($slice >= 0)
{
$pid = pcntl_fork(); if($pid > 0){
$childList[$pid] = 1; //父进程什么都不用做
}elseif($pid == 0){ //子进程不停的请求,直到所有短信发送完成
while(msg_receive($msgQueue,MSG_TYPE,$msgType,1024,$message))
{ if($cnt>intval($message))
{
printf("Slice id:%s,phone:%s rn",$slice,$phoneList[$message]);
$message = $message + 1;
msg_send($msgQueue,MSG_TYPE,$message);
}else
{ //通知其他进程一切都结束了
msg_send($msgQueue,MSG_TYPE,$cnt); exit();
}
}
}else
{ //程序发生错误也需要关闭程序
exit();
}
$slice--;
} // 等待所有子进程结束后回收资源
while(!empty($childList)){
$childPid = pcntl_wait($status); if ($childPid > 0){ unset($childList[$childPid]);
}
}
}const MSG_TYPE = 1;//创建消息队列$id = ftok(__FILE__,'m');
$msgQueue = msg_get_queue($id);
demo(range(0,900));/**运行结果,按大小输出
Slice id:1,phone:895
Slice id:1,phone:896
Slice id:2,phone:897
Slice id:3,phone:898
Slice id:3,phone:899
**/
共享内存与信号量
『共享内存』很容易理解,就是在内存中找一块区域,所有进程都能读写。『信号量』是系统提供的一种原子操作,进程在开启信号和结束信号之间拥有共享内存的『绝对占有』权,这样能有效的防止多个进程读取同一个资源时发生死锁。
function demo(array $phoneList){ global $shareMemory; global $signal;$cnt = count($phoneList); //测试数组大小
$slice = 3; //需要调用的进程数量
$childList = []; while($slice >= 0)
{
$pid = pcntl_fork(); if($pid > 0){
$childList[$pid] = 1; //父进程什么都不用做
}elseif($pid == 0){ while(true)
{ // 标记信号量,这里被我承包了
sem_acquire($signal); //检测共享内存是否存在
if (shm_has_var($shareMemory,SHARE_KEY)){ //从共享内存中拿数据
$val = shm_get_var($shareMemory,SHARE_KEY); if($val>=$cnt)
{
sem_release($signal); break;
}else
{
printf("Slice id:%s,phone:%s rn",$slice,$phoneList[$val]);
$val ++; //再将数据写入共享内存
shm_put_var($shareMemory,SHARE_KEY,$val);
}
}else{ // 无值会,先初始化
shm_put_var($shareMemory,SHARE_KEY,0);
} // 用完释放
sem_release($signal);
} exit();
}else
{ //程序发生错误也需要关闭程序
exit();
}
$slice--;
} // 等待所有子进程结束后回收资源
while(!empty($childList)){
$childPid = pcntl_wait($status); if ($childPid > 0){ unset($childList[$childPid]);
}
}
}const SHARE_KEY = 1;// 创建一块共享内存$shm_id = ftok(__FILE__,'a');
$shareMemory = shm_attach($shm_id);// 创建一个信号量$sem_id = ftok(__FILE__,'b');
$signal = sem_get($sem_id);
demo(range(0,900));// 释放共享内存与信号量shm_remove($shareMemory);
sem_remove($signal);/**运行结果,按大小输出
Slice id:1,phone:775
Slice id:3,phone:776
Slice id:3,phone:777
Slice id:3,phone:778
Slice id:0,phone:779
Slice id:0,phone:780
**/
管道
管道是比较常用的进程间通信手段,管道又分为匿名管道(pipe)与具名管道(mkfifo),匿名管道只能用于具有亲缘关系的进程间通信,而具名管道可以用于同一主机上任意进程。
pipe与mkfifo的主要差别是mkfifo会创建一个特殊的FIFO物理文件,这个FIFO文件其他进程都可以像读写一般文件一样读写。再写下去文章就太长了,之后写下一篇吧。
未完待续……
PS:所有代码都放到了GitHub:php_thread_demo
沟通和互动以及更多干货,欢迎关注新浪微博:@阿里云云栖社区