PHP 进程与线程 管道

假设我们现在有一个通过curl抓取网页内容的需求,需要抓取10个网页,url地址通过数组读取,每个curl耗时2s。如果我们通过for循环来抓取这10个网页,需要耗时20s,使用多进程我们可以将任务划分成5份,分别由5个进程执行,每个进程抓取2个url,并发执行,共耗时4s,效率提高5倍。

进程与线程的概念

进程是并发执行的程序在执行过程中分配和管理资源的基本单位。
线程是进程内的一个执行单元,是比进程更小的能独立运行的基本单位。线程(thread) 是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,线程自己不拥有系统资源,它与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。每一个程序都至少有一个线程,那就是程序本身,通常称为主线程。线程是程序中一个单一的顺序控制流程。 在单个程序中同时运行多个线程完成不同的工作,称为多线程。

进程与线程的区别

  • 最小单位

进程是资源分配最小单位,线程是程序执行的最小单位。计算机在执行程序时,会为程序创建相应的进程,进行资源分配时,是以进程为单位进行相应的分配。每个进程都有相应的线程,在执行程序时,实际上是执行相应的一系列线程。

  • 资源空间

进程有独立的地址空间,线程没有独立的地址空间。每启动一个进程,系统都会为其分配地址空间,建立数据表来维护代码段、堆栈段和数据段;而同一进程的线程则共享本进程的地址空间。

  • 资源归属

进程之间的资源是独立的;同一进程内的线程共享本进程的资源。

  • 执行过程

每个独立的进程有一个程序运行的入口和顺序执行序列。但是线程不独立执行,必须依存在应用程序执行。

  • 调度机制

线程是处理机调度的基本单位,但进程不是。由于程序执行的过程其实是执行具体的线程,那么处理机处理的也是程序相应的线程,所以处理机调度的基本单位是线程。

  • 系统开销

进程执行开销大,线程执行开销小

进程

PHP多进程只能通过php-cli 模式,所以对于web页面的请求,多进程的梦想破灭。适用场景:定时任务执行,且互斥耗时的任务(数据的批量插入,批量更新,日志的批量操作)设我想要启动5个进程,将80w的数据分成5个进程来做,每个进程处理50000,主进程等待所有子进程都结束了才退出:

<?php

$total = 800000;
$workers = 6;
$pageSize = 50000;
$j = 0;
process:
for ($i = $j + 0; $i < $j + $workers; ++$i) {
    $size = ($i + 1) * $pageSize;
    $pid = pcntl_fork();
    switch ($pid) {
        case -1:
            echo "fork失败".PHP_EOL;
            exit;
        case 0: //子进程
            $param = [
                'start' => $pageSize * $i,
                'end' => $size,
            ];
            $childPid = posix_getpid(); //获取子进程id
            task($i, $childPid, $param);
            exit; // 一定要退出,否则子进程会继承上下文继续执行
        default: //父进程
            break;
    }
    if ($size >= $total) {
        break;
    }
}

while ($size < $total) {
    $pid = pcntl_wait($status);
    echo "$pid,";
    if (pcntl_wifexited($status)) {
        echo "进程完成退出\n";
        $j = $i;
        $workers = 1;
        goto process;
    }
}

function task($taskId, $pid, $param)
{
    echo "task {$taskId} pid:$pid is over, exec rows: ".json_encode($param), PHP_EOL;
}

PHP自带的进程存在很多不足,如:

  • 没有提供进程间通信的功能
  • 不支持重定向标准输入和输出
  • 只提供了fork这样原始的接口,容易使用错误

popen管道

popen函数会创建一个管道,它允许你通过命令行执行程序并读取或写入数据。所以不会对PHP造成阻塞。但异步是有条件的,需要在command后面加上“&”,表示后台执行。

<?php

class ProcessPool
{
    public const MAX_PROCESS = 30;
    public const WRITE_OSS_TAG = 'test:month';

    public function __construct()
    {
        try {
            $i = 0;
            while (true) {
                $nums = self::currentProcessNum(self::WRITE_OSS_TAG);
                $nums = (int) $nums;
                if ($i > 0 && $nums <= 0) {
                    break;
                }
                $left = self::MAX_PROCESS - $nums;
                for ($j = 0; $j < $left; ++$j) {
                    $processCommend = 'nohup php bin/console '.self::WRITE_OSS_TAG.' > /dev/null 2>&1 &';
                    self::runProccess($processCommend);
                }
                ++$i;
            }
        } catch (InvalidArgumentException $e) {
        }
    }

    public function demo()
    {
        $command = 'nohup php bin/console '.self::WRITE_OSS_TAG.' > /dev/null 2>&1 &';
        self::runProccess($command);
        phone:
        $phoneCourse = self::currentProcessNum(self::WRITE_OSS_TAG);
        if ($phoneCourse >= 1) {
            sleep(60);
            echo self::WRITE_OSS_TAG.'还未执行完毕,请等待';

            goto phone;
        }
        echo self::WRITE_OSS_TAG.'还执行完毕';
    }

    /**
     * 检查进程总数.
     *
     * @param string $psName 进程名称
     *
     * @throws \InvalidArgumentException
     *
     * @return int
     */
    public static function currentProcessNum($psName)
    {
        // 防止命令注入
        $safeStr = escapeshellarg($psName);
        $cmd = popen("ps -ef | grep {$safeStr} | grep -v grep | wc -l", 'r');

        if (false === $cmd) {
            throw new \InvalidArgumentException(sprintf('开启检测【%s】总进程数失败', $psName));
        }

        // 读取所有输出内容
        $line = stream_get_contents($cmd);

        // 确保资源正确关闭
        if (false === pclose($cmd)) {
            throw new \InvalidArgumentException('关闭命令管道失败');
        }

        // 转换为整数并返回
        return (int) trim($line);
    }

    /**
     * 开启多进程.
     *
     * @param string $command
     *
     * @throws \InvalidArgumentException
     */
    public static function runProccess($command)
    {
        // 防止命令注入
        $safeCommand = escapeshellcmd($command);
        $out = popen($safeCommand, 'r');
        if (!$out) {
            throw new \InvalidArgumentException(sprintf('开启进程【%s】失败', $command));
        }
        $outString = stream_get_contents($out);
        if ($outString === false) {
            throw new \InvalidArgumentException('读取进程输出失败');
        }
        //pclose回收子进程,避免僵尸进程的产生
        pclose($out);

        return $outString;
    }
}

使用 ps -ef | grep 统计不到由 nohup 启动的进程。以下是详细的分析和解决方案:

  • nohup 启动的进程通常会在命令前加上 nohup 和重定向符号(如 > /dev/null 2>&1 &),这可能导致 ps -ef | grep 无法匹配到你预期的字符串。
  • nohup 启动的进程可能会在不同的 shell 环境下运行,或者其命令行参数可能与直接启动时有所不同。
  • 进程还没有成功挂起,统计太快?统计前sleep一秒

Swoole Process

    Swoole Process提供了比pcntl更强大的功能,更易用的API,使PHP在多进程编程方面更加轻松。

    Process提供了如下特性:

    • 基于Unix Socketsysvmsg消息队列的进程间通信,只需调用write/read或者push/pop即可
    • 支持重定向标准输入和输出,在子进程内echo不会打印屏幕,而是写入管道,读键盘输入可以重定向为管道读取数据
    • 配合Event模块,创建的PHP子进程可以异步的事件驱动模式
    • 提供了exec接口,创建的进程可以执行其他程序,与原PHP父进程之间可以方便的通信

    Swoole的Process模块内置了管道的方式用于进程间通信,在构建Process实例时只要开启了$pipe_type选项,Swoole底层会自动创建一个管道,这里需要说明的时,虽然名字上叫做管道,但实际上在新版Swoole中底层通信是通过UnixSock实现的,所以并不是真正意义上的Linux Pipe。每创建一个进程就会随着创建一个管道,主进程若要和目标进程通信,可以向目标进程的管道写入或读取数据。

    管道读写

    • swoole_process->write(string $data) 向进程的管道中写入数据
    • swoole_process->read(int $buffer_size = 8192) 从进程的管道中读取数据

    关闭重定向子进程的标准输入输出

    首先将所有子进程句柄保存到主进程的一个数组中,数组的下标为PID。当主进程向要和某个子进程通讯时,使用子进程的句柄向对应的管道中读写数据,即可实现进程间的管道通信。

    <?php
    //进程创建成功后回调处理
    function handle(swoole_process $worker){
        //从进程管道中读取数据
        $data = $worker->read();
        echo PHP_EOL."from master: {$data}";
        //向进程管道中写入数据
        $pipe = $worker->pipe;//子进程的管道编号
        $pid = $worker->pid;//子进程的PID
        $worker->write("hello master, this pipe is {$pipe}, pid is {$pid}");
        sleep(2);
        $worker->exit(0);
    }
    
    //进程数量
    $worker_num = 2;
    //重定向输入输出
    $redirect_stdin_stdout = false;
    //存放进程的数组
    $workers = [];
    //循环创建多进程
    for($i=0; $i<$worker_num; $i++){
        //创建进程
        $process = new swoole_process("handle", $redirect_stdin_stdout);
        //启动进程
        $pid = $process->start();
        //保存进程句柄
        $workers[$pid] = $process;
    }
    
    //主进程
    foreach($workers as $pid=>$process){
        //子进程句柄向自己的管道中写入数据
        $process->write("hello worker, this pid is {$pid}");
        //子进程句柄从自己的管道中读取数据
        $data = $process->read();
        echo PHP_EOL."from worker: {$data}".PHP_EOL;
    }

    运行

    $ php test.php
    
    from master: hello worker, this pid is 347
    from worker: hello master, this pipe is 4, pid is 347
    
    from master: hello worker, this pid is 348
    from worker: hello master, this pipe is 6, pid is 348

    开启重定向子进程的标准输入输出

    创建进程时设置$redirect_stdin_stdouttrue启用后,在进程内使用echo将不再打印到屏幕,而会写入到管道,读取键盘输入将变为从管道中读取数据,默认为阻塞读取

    <?php
    //进程创建成功后回调处理
    function handle(swoole_process $worker){
        //从进程管道中读取数据
        $data = $worker->read();
        echo PHP_EOL."from master: {$data}";
        //向进程管道中写入数据
        $pipe = $worker->pipe;//子进程的管道编号
        $pid = $worker->pid;//子进程的PID
        $worker->write("hello master, this pipe is {$pipe}, pid is {$pid}");
        sleep(2);
        $worker->exit(0);
    }
    
    //进程数量
    $worker_num = 2;
    //重定向输入输出
    $redirect_stdin_stdout = true;
    //存放进程的数组
    $workers = [];
    //循环创建多进程
    for($i=0; $i<$worker_num; $i++){
        //创建进程
        $process = new swoole_process("handle", $redirect_stdin_stdout);
        //启动进程
        $pid = $process->start();
        //保存进程句柄
        $workers[$pid] = $process;
    }
    
    //主进程
    foreach($workers as $pid=>$process){
        //子进程句柄向自己的管道中写入数据
        $process->write("hello worker, this pid is {$pid}");
        //子进程句柄从自己的管道中读取数据
        $data = $process->read();
        echo PHP_EOL."from worker: {$data}".PHP_EOL;
    }

     运行

    $ php test.php
    
    from worker:
    from master: hello worker, this pid is 350
    
    from worker:
    from master: hello worker, this pid is 351

    消息队列message queue

    Linux的消息队列是一系列保存在内核中的消息链表,消息队列中有一个msgKey,可以通过它访问不同的消息队列。消息队列有数据大小限制,默认为8192,可以通过内核进行修改。

    Swoole中使用消息队列

    • 通信模式:默认采用争抢模式,无法将消息投递给指定的子进程。
    • 新建消息队列后,主进程就可以使用。
    • 消息队列不可以和管道一起使用,也无法使用swoole event loop
    • 主进程中要调用wait()函数,否则子进程中调用pop()push()会报错。

    Swoole消息队列函数

    • swoole_process->useQueue()
    • swoole_process->push(string $data)
    • swoole_process->pop(int $max_size = 8192)

    案例

    <?php
    //进程创建成功后回调处理
    function handle(swoole_process $worker){
        $recv = $worker->pop();
        echo PHP_EOL."from master: {$recv}";
        sleep(2);
        $worker->exit(0);
    }
    
    //进程数量
    $worker_num = 2;
    //重定向输入输出
    $redirect_stdin_stdout = false;
    //存放进程的数组
    $workers = [];
    //循环创建多进程
    for($i=0; $i<$worker_num; $i++){
        //创建进程
        $process = new swoole_process("handle", $redirect_stdin_stdout);
        //使用消息队列
        $process->useQueue();
        //启动进程
        $pid = $process->start();
        //保存进程句柄
        $workers[$pid] = $process;
    }
    
    //主进程
    foreach($workers as $pid=>$process){
        $process->push("hello worker, this pid is {$pid}");
    }
                  
    for($i=0; $i<$worker_num; $i++){
        $ret = swoole_process::wait();
        $pid = $ret["pid"];
        unset($workers[$pid]);
        echo PHP_EOL."worker pid is {$pid} exit";
    }

    运行

    $ php test.php
    
    from master: hello worker, this pid is 368
    from master: hello worker, this pid is 369
    worker pid is 368 exit
    worker pid is 369 exit
    <?php
    
    /**
     * 动态进程池,类似fpm
     * 动态新建进程
     * 有初始进程数,最小进程数,进程不够处理时候新建进程,不超过最大进程数.
     */
    
    // 一个进程定时投递任务
    
    /**
     * 1. tick
     * 2. process及其管道通讯
     * 3. event loop 事件循环.
     */
    class ProcessPool
    {
        private $pool;
    
        /**
         * @var swoole_process[] 记录所有worker的process对象
         */
        private $workers = [];
    
        /**
         * @var array 记录worker工作状态
         */
        private $usedWorkers = [];
    
        /**
         * @var int 最小进程数
         */
        private $minWokerNum = 5;
    
        /**
         * @var int 初始进程数
         */
        private $startWorkerNum = 10;
    
        /**
         * @var int 最大进程数
         */
        private $maxWokerNum = 20;
    
        /**
         * 进程闲置销毁秒数.
         *
         * @var int
         */
        private $idleSeconds = 5;
    
        /**
         * @var int 当前进程数
         */
        private $currNum;
    
        /**
         * 闲置进程时间戳.
         *
         * @var array
         */
        private $activeTime = [];
    
        private $callback = null;
    
        const JOB_EXIT_CODE = 'exit';
        const JOB_COMPLETE_CODE = 'complete';
    
        public function __construct($callback)
        {
            $this->callback = $callback;
    
            $this->pool = new swoole_process(function () {
                // 循环建立worker进程
                for ($i = 0; $i < $this->startWorkerNum; ++$i) {
                    $this->createWorker();
                }
                echo '初始化进程数:'.$this->currNum.PHP_EOL;
                // 每秒定时往闲置的worker的管道中投递任务
                swoole_timer_tick(1000, function ($timerId) {
                    static $count = 0;
                    ++$count;
                    $needCreate = true;
                    foreach ($this->usedWorkers as $pid => $used) {
                        if (0 == $used) {
                            $needCreate = false;
                            $this->workers[$pid]->write($count.' job');
                            // 标记使用中
                            $this->usedWorkers[$pid] = 1;
                            $this->activeTime[$pid] = time();
                            break;
                        }
                    }
                    foreach ($this->usedWorkers as $pid => $used) { // 如果所有worker队列都没有闲置的,则新建一个worker来处理
                        if ($needCreate && $this->currNum < $this->maxWokerNum) {
                            $newPid = $this->createWorker();
                            $this->workers[$newPid]->write($count.' job');
                            $this->usedWorkers[$newPid] = 1;
                            $this->activeTime[$newPid] = time();
                        }
                    }
    
                    // 闲置超过一段时间则销毁进程
                    foreach ($this->activeTime as $pid => $timestamp) {
                        if ((time() - $timestamp) > $this->idleSeconds && $this->currNum > $this->minWokerNum) {
                            // 销毁该进程
                            if (isset($this->workers[$pid]) && $this->workers[$pid] instanceof swoole_process) {
                                $this->workers[$pid]->write(self::JOB_EXIT_CODE);
                                unset($this->workers[$pid]);
                                $this->currNum = count($this->workers);
                                unset($this->usedWorkers[$pid]);
                                unset($this->activeTime[$pid]);
                                echo "{$pid} destroyed\n";
                                break;
                            }
                        }
                    }
    
                    echo "任务{$count}/{$this->currNum}\n";
    
                    if ($this->maxWokerNum == $count) {
                        foreach ($this->workers as $pid => $worker) {
                            $worker->write(self::JOB_EXIT_CODE);
                        }
                        // 关闭定时器
                        swoole_timer_clear($timerId);
                        // 退出进程池
                        $this->pool->exit(0);
                        exit();
                    }
                });
            });
    
            $masterPid = $this->pool->start();
            echo "Master $masterPid start\n";
    
            while ($ret = swoole_process::wait()) {
                $pid = $ret['pid'];
                echo "process {$pid} existed\n";
            }
        }
    
        /**
         * 创建一个新进程.
         *
         * @return int 新进程的pid
         */
        public function createWorker()
        {
            $workerProcess = new swoole_process(function (swoole_process $worker) {
                // 给子进程管道绑定事件
                swoole_event_add($worker->pipe, function ($pipe) use ($worker) {
                    $data = trim($worker->read());
                    if (ProcessPool::JOB_EXIT_CODE == $data) {
                        $worker->exit(0);
                        exit();
                    }
                    if (is_callable($this->callback)) {
                        call_user_func($this->callback, $worker, $data); //回调
                    }
                    // 返回结果,表示空闲
                    $worker->write(self::JOB_COMPLETE_CODE);
                });
            });
    
            $workerPid = $workerProcess->start();
    
            // 给父进程管道绑定事件
            swoole_event_add($workerProcess->pipe, function ($pipe) use ($workerProcess) {
                $data = trim($workerProcess->read());
                if (self::JOB_COMPLETE_CODE == $data) {
                    // 标记为空闲
                    //echo "{$workerProcess->pid} 空闲了\n";
                    $this->usedWorkers[$workerProcess->pid] = 0;
                }
            });
    
            // 保存process对象
            $this->workers[$workerPid] = $workerProcess;
            // 标记为空闲
            $this->usedWorkers[$workerPid] = 0;
            $this->activeTime[$workerPid] = time();
            $this->currNum = count($this->workers);
    
            return $workerPid;
        }
    }
    
    new ProcessPool(function ($worker, $data) {
        echo "{$worker->pid} 正在处理 {$data}\n";
        sleep(5);
    });

    线程

    使用多线程就需要安装 pthread 多线程扩展,需要php5.3或以上。 

    Thread类和方法
    PHP 将线程封装成Thread 类,线程的创建通过实例化一个线程对象来实现,由于类的封装性,变量的使用只能通过构造函数传入,而线程运算结果也需要通过类变量传出,下面介绍几个常用的 Thread 类方法。

    run():抽象方法,每个线程都要实现此方法,线程开始运行后,此方法中的代码会自动执行。
    start():在主线程内调用此方法以开始运行一个线程。
    join():各个线程相对于主线程都是异步执行,调用此方法会等待线程执行结束。
    kill():强制线程结束。
    isRunning():返回线程的运行状态,线程正在执行run()方法的代码时会返回 true。

    <?php
    
    class AsyncOperation extends \Thread
    {
        public function __construct($arg)
        {
            $this->arg = $arg;
        }
    
        public function run()
        {
            if ($this->arg) {
                printf("Hello %s\n", $this->arg);
            }
        }
    }
    
    $thread = new AsyncOperation("World");
    if ($thread->start()) {
        $thread->join();
    }

    成功输出Hello World说明成功

    多线程编程中对数据库更新操作需要注意的是,有些场景,你需要控制同一时刻只能有一个线程对数据库做Update, Delete, Insert,否则数据容易出错。例如下面的操作,你会发现程序运行完成后数据字段没有任何变化。这是因为线程间相互覆盖对方之前更新的数据。解决方法有两种,一种是外部实现排他锁,一种是在数据库内部实现,通过事物处理,解决线程资源争夺,相互覆盖的问题

    <?php
    class My extends Thread{
        private $balance;
        public function __construct($balance){
            $this->balance = $balance;
        }
        public function draw($cost){
            $this->lock();
            if($this->balance >= $cost){
                usleep(500);
                $this->balance -= $cost;
                echo Thread::getCurrentThreadId() . "__get__${cost},now the balance is:{$this->balance}<br/>"; 
            }
            else 
                echo Thread::getCurrentThreadId() . "__get fail__,now the balance is :{$this->balance}<br/>";
            $this->unlock();
        }
    }

    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

    当前余额3.43前往充值 >
    需支付:10.00
    成就一亿技术人!
    领取后你会自动成为博主和红包主的粉丝 规则
    hope_wisdom
    发出的红包
    实付
    使用余额支付
    点击重新获取
    扫码支付
    钱包余额 0

    抵扣说明:

    1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
    2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

    余额充值