swoole源码学习——协程的概念,yield协程和原生协程的实现(上)

时间:2021-09-18 23:29:26
协程的概念
协程( Coroutine)又名纤程,是一种用户态的轻量级线程。协程不受内核调度,协程的切换完全由程序自己掌控,操作系统对协程无感知。协程拥有自己的寄存器上下文和栈。协程调度切换时(通常是协程主动让出CPU执行权),将寄存器上下文和栈保存,在切换回来时,再恢复先前保存的寄存器上下文和栈。

php中基于yield关键字的协程

php从5.5版起增加了yield关键字。使用了yield关键字的函数又被成为生成器函数。与return关键字不同的是,yield关键字实际上返回的是一个 生成器对象(Generator class)而非一个值,zend引擎会为这个 生成器对象开辟一块独立的堆栈空间,从而使得每一个生成器对象可以保存自己的状态。

生成器对象每次在收到迭代的指令后,会从之前的中断处即(yield关键字标识的地方)重新执行,直到再次遇到yield关键字,这是便保存自己当前的状态并暂时中断。

下面的代码中,在loop函数里使用了yield关键字,每次调用这个生成器函数时,都会从之前的中断处执行。
<?php
function loop($start, $end, $step = 1) {
    for ($i = $start; $i <= $end; $i += $step) {
        yield $i;
    }
}

$loop = loop(1, 10);
var_dump($loop); // 返回的是一个生成器对象 object(Generator)
var_dump($loop->current()); //获取生成器对象当前的值,输出1
$loop->next();//使生成器对象运行到下一个yield处
var_dump($loop->current()); //输出2

foreach (loop(1, 5) as $num) { //也可以使用foreach进行迭代
    echo $num, "\n";
}



我们知道,cpu(单核)实际上在一个时刻只能执行一个任务,但为了能让计算机用户觉得任务好像是在同时运行的(比如一边编辑文档一边通过浏览器看新闻),cpu需要在多个任务(进程)之间切换。而cpu处理哪一个任务则由操作系统决定,操作系统可以剥夺进程的执行权,将cpu执行权分配给其他进程。

而基于协程实现的"并发执行"(不是真的并发执行),则是多任务协作式的。当前正在运行的某个任务完成了它目前所能做的工作后,自动让出cpu资源,将控制权交还给调度器。

这两种方式(多任务抢占式和多任务协作式)不变的是,它能使进程具有“ 对称切换能力 ”,也就是进程a可以切换到进程b,进程b也可以再切换到进程a(区别于传统的父子函数式的调用)。
yield之所以能在php层面实现协程,就是因为yield关键字让函数可以具有多个“返回点”,使函数可以对称式地切换。

下面是一个简单的多任务协作的例子:

首先定义任务类
每个任务有自己的编号,和一个是生成器对象的成员变量,每次调用任务的run方法时就对生成器对象进行迭代。
<?php
class Task {
    protected $taskId;
    protected $coroutine;
    protected $sendValue = null;
    protected $beforeFirstYield = true;

    public function __construct($taskId, Generator $coroutine) {
        $this->taskId = $taskId;
        $this->coroutine = $coroutine;
    }

    public function getTaskId() {
        return $this->taskId;
    }

    public function setSendValue($sendValue) {
        $this->sendValue = $sendValue;
    }

    public function run() {
        if ($this->beforeFirstYield) {
            $this->beforeFirstYield = false;
            return $this->coroutine->current();
        } else {
            $retval = $this->coroutine->send($this->sendValue);
            $this->sendValue = null;
            return $retval;
        }
    }

    public function isFinished() {
        return !$this->coroutine->valid();
    }
}


然后是调度器类

调度器通过newTask方法创建新任务并将其入队,当调用run方法运行时,遍历任务队列并逐个执行,然后检测任务(协程)是否执行结束,如果任务(协程)还需要再次执行,那么将其重新入队,等待下一次执行。
<?php
class Scheduler {
    protected $maxTaskId = 0;
    protected $taskMap = []; // taskId => task
    protected $taskQueue;

    public function __construct() {
        $this->taskQueue = new SplQueue();
    }

    public function newTask(Generator $coroutine) {
        $tid = ++$this->maxTaskId;
        $task = new Task($tid, $coroutine);
        $this->taskMap[$tid] = $task;
        $this->schedule($task);
        return $tid;
    }

    public function schedule(Task $task) {
        $this->taskQueue->enqueue($task);
    }

    public function run() {
        while (!$this->taskQueue->isEmpty()) {
            $task = $this->taskQueue->dequeue();
            $task->run();

            if ($task->isFinished()) {
                unset($this->taskMap[$task->getTaskId()]);
            } else {
                $this->schedule($task);
            }
        }
    }
}



然后是task1和task2(虽然意义不大)
运行后可以看到两个任务是交替执行的。
<?php
function task1() {
    for ($i = 1; $i <= 10; ++$i) {
        echo "task 1 iteration $i.\n";
        yield;
    }
}

function task2() {
    for ($i = 1; $i <= 5; ++$i) {
        echo "task 2 iteration $i.\n";
        yield;
    }
}

$scheduler = new Scheduler;

$scheduler->newTask(task1());
$scheduler->newTask(task2());

$scheduler->run();
/*output:
task 1 iteration 1.
task 2 iteration 1.
task 1 iteration 2.
task 2 iteration 2.
task 1 iteration 3.
task 2 iteration 3.
task 2 iteration 4.
task 2 iteration 5.
*/


由于篇幅原因swoole2原生协程的实现将放在下篇讲述