协程的概念
协程(
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原生协程的实现将放在下篇讲述