协程的概念
协程( 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. */
参考与引用
Cooperative multitasking using coroutines (in PHP!) 22.