php中使用协程来提高代码执行效率

php中使用协程来提高代码执行效率

php中使用协程来提高代码执行效率

协程是最近几年新出来的一种技术,主要解决线程在进行大并发处理的时候io等待造成的资源等待问题,通过程序员自己进行调度解决的一种机制。

一、什么是协程

先来理清一下进程、线程、协程的区别

进程是资源分配的单位,真正执行代码的是线程,操作系统真正调度的是线程,每个CPU下同一时刻只能处理一个进程。
,进程就是二进制可执行文件在计算机内存里的一个运行实例,就好比你的.exe文件是个类,进程就是new出来的那个实例。
进程的切换需要进行系统调用,CPU要保存当前进程的各个信息,同时还会使CPUCache被废掉,所以进程没有线程效率高,进程占用资源多,线程占用资源少,比线程更少的是协程。

线程简单理解就是一个『微进程』,专门跑一个函数(逻辑流)。所以我们就可以在编写程序的过程中将可以同时运行的函数用线程来体现了。
线程有两种类型,一种是由内核来管理和调度。
我们说,只要涉及需要内核参与管理调度的,代价都是很大的。这种线程其实也就解决了当一个进程中,某个正在执行的线程遇到阻塞,我们可以调度另外一个可运行的线程来跑,但是还是在同一个进程里,所以没有了进程切换。
还有另外一种线程,他的调度是由程序员自己写程序来管理的,对内核来说不可见。这种线程叫做『用户空间线程』。

协程依赖于线程、线程依赖于进程,进程一死线程必挂,线程一挂协程必死,协程可以理解就是一种用户空间线程。

一般不用多进程,可以考虑使用多线程,如果多线程里面有很多网络请求,网络可能会有堵塞,此时用协程比较合适。

协程,有几个特点:
协同,因为是由程序员自己写的调度策略,其通过协作而不是抢占来进行切换
在用户态完成创建,切换和销毁
从编程角度上看,协程的思想本质上就是控制流的主动让出(yield)和恢复(resume)机制
generator经常用来实现协程

php中使用协程来提高代码执行效率

二、为何使用协程

使用协程可以提高cpu的利用率,将同步的执行程序变成异步,有点类似JavaScript中的异步,减少程序的等待时间,

比如如果我们要抓取页面,执行curl去请求网页,使用多线程去执行的话,会有线程的开销,cpu的上下文切换,浪费了时间,但是如果采用协程的话,一个线程就可以搞定了,在线程中创建协程,并进行调度,解决了资源浪费与等待问题。

三、怎么编写php协程

协程需要程序员自己去编写调度机制,下面我们来看这个机制怎么写。

<?php
/**
 * Task任务类
 */
class Task
{
    protected $taskId;
    protected $coroutine;
    protected $beforeFirstYield = true;
    protected $sendValue;

    /**
     * Task constructor.
     * @param $taskId
     * @param Generator $coroutine
     */
    public function __construct($taskId, Generator $coroutine)
    {
        $this->taskId = $taskId;
        $this->coroutine = $coroutine;
    }

    /**
     * 获取当前的Task的ID
     * 
     * @return mixed
     */
    public function getTaskId()
    {
        return $this->taskId;
    }

    /**
     * 判断Task执行完毕了没有
     * 
     * @return bool
     */
    public function isFinished()
    {
        return !$this->coroutine->valid();
    }

    /**
     * 设置下次要传给协程的值,比如 $id = (yield $xxxx),这个值就给了$id了
     * 
     * @param $value
     */
    public function setSendValue($value)
    {
        $this->sendValue = $value;
    }

    /**
     * 运行任务
     * 
     * @return mixed
     */
    public function run()
    {
        // 这里要注意,生成器的开始会reset,所以第一个值要用current获取
        if ($this->beforeFirstYield) {
            $this->beforeFirstYield = false;
            return $this->coroutine->current();
        } else {
            // 我们说过了,用send去调用一个生成器
            $retval = $this->coroutine->send($this->sendValue);
            $this->sendValue = null;
            return $retval;
        }
    }
}
?>

所以Task的构造函数中就是接收一个闭包函数,我们命名为coroutine。

接下来就是Scheduler这个重点核心部分,他扮演着调度员的角色。

<?php
/**
 * Class Scheduler
 */
Class Scheduler
{
    /**
     * @var SplQueue
     */
    protected $taskQueue;
    /**
     * @var int
     */
    protected $tid = 0;

    /**
     * Scheduler constructor.
     */
    public function __construct()
    {
        /* 原理就是维护了一个队列,
         * 前面说过,从编程角度上看,协程的思想本质上就是控制流的主动让出(yield)和恢复(resume)机制
         * */
        $this->taskQueue = new SplQueue();
    }

    /**
     * 增加一个任务
     *
     * @param Generator $task
     * @return int
     */
    public function addTask(Generator $task)
    {
        $tid = $this->tid;
        $task = new Task($tid, $task);
        $this->taskQueue->enqueue($task);
        $this->tid++;
        return $tid;
    }

    /**
     * 把任务进入队列
     *
     * @param Task $task
     */
    public function schedule(Task $task)
    {
        $this->taskQueue->enqueue($task);
    }

    /**
     * 运行调度器
     */
    public function run()
    {
        while (!$this->taskQueue->isEmpty()) {
            // 任务出队
            $task = $this->taskQueue->dequeue();
            $res = $task->run(); // 运行任务直到 yield

            if (!$task->isFinished()) {
                $this->schedule($task); // 任务如果还没完全执行完毕,入队等下次执行
            }
        }
    }
}
?>

这样我们基本就实现了一个协程调度器。
你可以使用下面的代码来测试:

<?php
function task1() {
    for ($i = 1; $i <= 10; ++$i) {
        echo "This is task 1 iteration $i.\n";
        yield; // 主动让出CPU的执行权
    }
}
 
function task2() {
    for ($i = 1; $i <= 5; ++$i) {
        echo "This is task 2 iteration $i.\n";
        yield; // 主动让出CPU的执行权
    }
}
 
$scheduler = new Scheduler; // 实例化一个调度器
$scheduler->addTask(task1()); // 添加不同的闭包函数作为任务
$scheduler->addTask(task2());
$scheduler->run();
?>

ok,这样就实现了php的协程,平时在php中应用场景如下

<?php
function task1() {
        /* 这里有一个远程任务,需要耗时10s,可能是一个远程机器抓取分析远程网址的任务,我们只要提交最后去远程机器拿结果就行了 */
        remote_task_commit();
        // 这时候请求发出后,我们不要在这里等,主动让出CPU的执行权给task2运行,他不依赖这个结果
        yield;
        yield (remote_task_receive());
        ...
}
 
function task2() {
    for ($i = 1; $i <= 5; ++$i) {
        echo "This is task 2 iteration $i.\n";
        yield; // 主动让出CPU的执行权
    }
}
?>

那么如何在协程中调用另外一个协程呢,php7提供了yield from

<?php
function echoTimes($msg, $max) {
    for ($i = 1; $i <= $max; ++$i) {
        echo "$msg iteration $i\n";
        yield;
    }
}
function task1()
{
    yield from echoTimes('bar', 5);
}

?>


{{collectdata}}

网友评论0