PHP进程间通信探究

PHP进程探究

PHP作为解释器运行通过线程或者进程都能实现(如果使用Apache,那么就可能使用多线程模型。使用php-fpm,就是使用多进程模型,这里以多进程模型解释)。服务器每接收到一个请求就要起一个PHP进程,平均一个PHP进程消耗内存2M左右(默认最大为8M,参数可以设置)。独立的进程让PHP能专一的做自己的解释工作,程序员也从复杂的代码逻辑中走出来,不用担心资源的竞争和各种锁问题。独立进程虽好但这也导致想通过多进程或者异步来提速成本非常的高(主要是开发难度)。如果一定要通过PHP实现多进程和异步其实是很容易做到的。

PHP有很多第三方扩展,比如Swoole能让PHP像Node一样实现异步。PHP官方扩展库pcntl_*能很简单的实现多进程。扩展虽好,但实际应用时切忌要慎重,便利的同时风险也来了。比如对多进程的控制,处理不好很容易导致程序死锁,CPU内存爆表、服务器宕机。异步回调的Coding方式与PHP本身的编程思想有一定出入,驾驭不好也是灾难。

当然也不能说的太吓人,在实际的项目中我们有很多场景不得不考虑通过多进程或者异步来优化程序。这里举一个很常见的例子『发送消息通知』,比如短信和邮件。这里说一个实际的场景:企业需要给200W用户发短信通知,短信接口支持最大100次/秒的调用频率,短信接口每次调用耗时300毫秒。如果单进程跑脚本的话,需要7天才能把短信发完。如果我们起30个进程,每秒能发送100条短信,6个小时内能发完,能提速30倍。优化方案确定之后,我们再看如何通过PHP去实现这样一个脚本。

一. pcntl扩展初探;

  1. 通过pcntl扩展创建多进程,参见如下代码;

    function demo(array $phoneList){
        $cnt = count($phoneList);  //测试数组大小
        $slice = 30;  //需要调用的进程数量
        $master = array_chunk($phoneList,floor($cnt/$slice));
        $childList = [];
    
        while($slice >= 0)
        {
            $pid = pcntl_fork();
            if($pid > 0){
                $childList[$pid] = 1;
                //$pid>0表示当前还在执行父进程的代码
                //这里最好啥都不做,每次执行pcntl_fork都会执行这里的代码。
                //这里的代码执行完之后 会将$pid设置为0,然后jump到pcntl_fork代码之后,重新做判断;
            }elseif($pid == 0){
                //这里写我们的逻辑
                foreach($master[$slice] as $val)
                {
                    //这里发生短信
                    echo sprintf("%s Child:%s  \r\n",$slice,$val);
                }
                //子进程执行完之后务必需要关闭;
                exit();
            }else
            {
                //程序发生错误也需要关闭程序
                exit();
            }
            $slice--;
        }
    
        // 等待所有子进程结束后回收资源
        while(!empty($childList)){
            $childPid = pcntl_wait($status);
            if ($childPid > 0){
                unset($childList[$childPid]);
            }
        }
    }
    
    /** 运行的结果如下,phone不是连续的
    Slice id:19,phone:66558
    Slice id:23,phone:79921
    Slice id:19,phone:66559
    Slice id:23,phone:79922
    Slice id:19,phone:66560
    Slice id:23,phone:79923
    Slice id:19,phone:66561
    Slice id:23,phone:79924
    Slice id:19,phone:66562
    Slice id:23,phone:79925
    **/
    

    通过pcntl扩展,几句代码就使用多进程将发消息通知的功能提速了30倍。不过这么简单的多进程编码,我为什么会在文章开始形容的如此复杂呢?

    重点和难点还是进程间通信,因为我们给用户发短信的每个子进程是相对独立的,进程之间没有通信,不会互相传递数据状态。所以不会发生资源抢占与锁问题。假如需求发生变化,我们需要按用户的活跃度高低给用户发短信,该怎么做?

    通俗点解释如下:一个盘子里有30个苹果,需要发给30个人,由3个人负责发苹果。最简单的办法就是我们先把苹果分成3份,3个人一人一份,很快就能发完。但是如果我们要按照苹果的大小顺序去发,把大苹果先发出去,此时我们就没办法分成3份了,只能三个人互相去挣当前最大的,很容易就打起来。那该怎么做呢?最常见的办法就是使用一个工具把所有苹果按由大到下的顺序放在里面,每次只能取一个,这样就解决了资源抢占的问题。

    关于进程间资源抢占的问题非常的复杂,编码难度非常高,这也是为什么很少使用PHP跑多进程的原因。当需要用到多进程时我们更愿意去使用Python或者Java,它们对多线程封装的更好。需要重点说的是PHP并不是不能写多进程的程序,也不是像其他人说的不稳定,而是编码费时,维护成本高。

二. 进程间通信

常见的进程通信方式有:消息队列、共享内存与信号量、管道、socket,我将一一举例说明。

  1. 消息队列

    『消息队列』是在消息的传输过程中保存消息的容器。消息队列管理器相当于消息发送者和接收者的中介。消息队列的主要目的是创建路由并且保证消息可靠传递;如果发送消息时接收者不可用,消息队列会保留消息,直到有人接收它。

    消息队列可提供临时存储的功能并且能保证消息可靠的传递,我们正好使用它实现进程间通信。当然消息队列不单单用于进程间通信,他的应用领域非常广。比如消息队列非常适用于解决消费者和生产者的问题,因为生产者和消费者之间总会存在『速度差』。比如生产者突然少了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 \r\n",$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
    **/
    
    
  2. 共享内存与信号量

    『共享内存』很容易理解,就是在内存中找一块区域,所有进程都能读写。『信号量』是系统提供的一种原子操作,进程在开启信号和结束信号之间拥有共享内存的『绝对占有』权,这样能有效的防止多个进程读取同一个资源时发生死锁。

    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 \r\n",$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
    **/
    
  3. 管道

    管道是比较常用的进程间通信手段,管道又分为匿名管道(pipe)与具名管道(mkfifo),匿名管道只能用于具有亲缘关系的进程间通信,而具名管道可以用于同一主机上任意进程。

    pipe与mkfifo的主要差别是mkfifo会创建一个特殊的FIFO物理文件,这个FIFO文件其他进程都可以像读写一般文件一样读写。再写下去文章就太长了,之后写下一篇吧。

    未完待续……

PS:所有代码都放到了GitHub:php_thread_demo

时间: 2024-09-08 22:32:44

PHP进程间通信探究的相关文章

写了一个简单的NodeJS实现的进程间通信的例子

1. cluster介绍 大家都知道nodejs是一个单进程单线程的服务器引擎,不管有多么的强大硬件,只能利用到单个CPU进行计算.所以,有人开发了第三方的cluster,让node可以利用多核CPU实现并行.随着nodejs的发展,让nodejs上生产环境,就必须是支持多进程多核处理!在V0.6.0版本,Nodejs内置了cluster的特性.自此,Nodejs终于可以作为一个独立的应用开发解决方案,映入大家眼帘了. cluster是一个nodejs内置的模块,用于nodejs多核处理.clu

数据库进程间通信解决方案

数据库进程间通信解决方案 数据库与其他第三方应用程序进程间通信解决方案 Mr. Neo Chen (netkiller), 陈景峰(BG7NYT) 中国广东省深圳市龙华新区民治街道溪山美地518131+86 13113668890+86 755 29812080<netkiller@msn.com> $Id: MySQL-plugin.xml 587 2013-12-16 14:00:00Z netkiller $ 版权 2011, 2012, 2013 http://netkiller.gi

探究推荐引擎瞬间被“秒”背后:究竟是什么让用户接踵而至?

6月16日,阿里云技术专家郑重(卢梭)将做客,直播分享<技术实战:21天搭建推荐系统>,报名地址:https://yq.aliyun.com/webinar/join/14 推荐引擎官网 "我是做电商CRM的,市场中有非常多的CRM在相互竞争,必须要找到一个能让自己的产品区别于竞争对手,甚至优于对手的核心能力.现在产品的未来都压在推荐引擎上,我需要你们帮助定制出这项能力."一位企业的负责人如此坦言. 5月18日上午11点,推荐引擎新版上线,在限量提供折扣抢购后,所有的产品瞬

在.NET中使用命名管道完成进程间通信

进程 你曾经需要在同一台机器的两个.NET应用程序间进行数据交换吗?例如,一个Web站点和一个Windows服务?.NET框架提供了几种好的选择来完成进程间通信(IPC):Web Service,Remoting.最快的是Remoting,因为它使用TCP通道和二进制格式. 然而,如果需要频繁地从一个应用程序调用另外一个应用程序,并且你主要关心的是性能,Remoting还是显得慢了一点.让Remoting变慢的,不是协议,而是序列化. 通常来说,Remoting是很不错的,但如果仅限于本地机器的

探究百度推广和SEO优化哪一个才适合企业长久使用

现在,很多企业知道除了百度竞价推广能够使得企业获得很多的客户以外,现在很流行也被更多企业知道的SEO优化,也更加得到各大中小企业的喜爱. 然而在很多中小型企业网站建设中,宁愿花费一两仟去制作网站,投入大量的金钱去做百度推广.但是等到推广了很长一段时间之后,很想不通,为什么没有客户达成交易.我们有一个客户,是做樱花热水器维修的,之前就是每年花费好几万在推广竞价上.但是得到的回答却大相径庭.等到他知道优化的害处之后,其实得到的只是教训.花费如此巨大金额的钱买来的教训,还真是够刻骨铭心的. 后来,咨询

网站快照日期更新内容不更新的原因探究

余斗在平时的优化过程中并不看重快照这一项,百度站长社区曾经也发表过声明<关于百度快照问题的若干说明>,详细的指出了百度是如何更新一个网站的快照以及快照更新的意义,余斗这里就不去一一道来,有兴趣的可以去百度站长社区的资料区去看看! 但是最近余斗发现一个很奇怪的现象:余斗的个人博客快照每天都在更新,但是预览快照却发现内容是很久以前的,并不是即时的.   快照更新日期与提取内容 上图是余斗博客的快照更新日期,大家可以看到是隔天更新的,而且提取的内容也是余斗博客昨天才发的文章,说明百度蜘蛛确实抓取了首

探究在SEO中URL的结构优化

SEO是一项很复杂的工作,有很多细节.今天上海SEO顾辉明就与大家探究下网站URL的优化在SEO过程中的作用. 首先,URL有三要素: 1.简短易记,有利于传播性. 2.稳定长久,这个不用我多说了. 3.URL含有关键词,这个也是我重点要说的. 我们先来看一个例子,百度搜索SEO.   我们可以看到,在标题和描述里面SEO都飘红了,在URL中SEO加粗了,但是没有飘红,其实这是一样的,都是飘红算法,只加粗没有飘红是为了美观.从这就能说明一点,URL中含有关键词是可以给SEO加分的. 但是这里要注

从苏轼的哲思探究SEO优化人员的四种境界

说道网站SEO优化,给我们广大SEO优化工作者带来了巨大的思想上的折磨,因网站的排名上升而欣喜若狂,因网站排名的突然下降而痛不欲生,在网站排名的大起大落中,演绎着人生的悲欢爱恨,而这一切的过程就好像苏轼自己的人生境界,下面我们就从苏轼被贬黄州的人生境界来分享一下笔者对SEO优化人员的思想境界的探究! 有恨无人省 当SEO优化人员自己手中的网站排名通过自己的努力始终不见有上升的起色,自然就会遭遇领导或者客户的埋怨,特别是有些老板对于SEO优化的了解不够,认为三两个星期就能够将某个相对热门的关键词做

android如何实现进程间通信

一.使用bundle 当我们在一个进程中启动了另一个进程的activity.service.receiver,我们就可以在bundle中附加我们需要传输给远程进程的信息并通过intent发送出去. 我们传输的数据要必须能被序列化. 以上原理都是bundle 当然service也是支持通过intent启动来传递数据的 这种方案是一种最最简单的进程间通信 二.使用文件共享 三.使用Messenger 1.简介 Messenger的底层是aidl 2.Aidl和messager的区别 原因是AIDL可