基本概念
援引几个博客上的话:
对于单核处理器,多进程实现多任务的原理是让操作系统给一个任务每次分配一定的 CPU时间片,然后中断、让下一个任务执行一定的时间片接着再中断并继续执行下一个,如此反复。由于切换执行任务的速度非常快,给外部用户的感受就是多个任务的执行是同时进行的。多进程的调度是由操作系统来实现的,进程自身不能控制自己何时被调度,也就是说:
进程的调度是由外层调度器抢占式实现的
而协程要求当前正在运行的任务自动把控制权回传给调度器,这样就可以继续运行其他任务。这与『抢占式』的多任务正好相反,抢占多任务的调度器可以强制中断正在运行的任务, 不管它自己有没有意愿。『协作式多任务』在 Windows 的早期版本 (windows95)和 Mac OS 中有使用,不过它们后来都切换到『抢占式多任务』了。理由相当明确:如果仅依靠程序自动交出控制的话,那么一些恶意程序将会很容易占用全部 CPU时间而不与其他任务共享。
协程的调度是由协程自身主动让出控制权到外层调度器实现的
协程可以理解为纯用户态的线程,通过协作而不是抢占来进行任务切换。相对于进程或者线程,协程所有的操作都可以在用户态而非操作系统内核态完成,创建和切换的消耗非常低。简单的说 Coroutine(协程) 就是提供一种方法来中断当前任务的执行,保存当前的局部变量,下次再过来又可以恢复当前局部变量继续执行。
转载来源:http://www.jianshu.com/p/edef1cb7fee6
然后看下韩天峰对于应用协程的看法:
当程序员还沉浸在解决C10K问题带来的成就感时,一个新的问题被抛出了。异步嵌套回调太TM难写了。尤其是Node.js层层回调,缩进了几十层,要把程序员逼疯了。于是一个新的技术被提出来了,那就是协程(coroutine)。这个技术本质上也是异步非阻塞技术,它是将事件回调进行了包装,让程序员看不到里面的事件循环。程序员就像写阻塞代码一样简单。比如调用 client->recv() 等待接收数据时,就像阻塞代码一样写。实际上是底层库在执行recv时悄悄保存了一个状态,比如代码行数,局部变量的值。然后就跳回到EventLoop中了。什么时候真的数据到来时,它再把刚才保存的代码行数,局部变量值取出来,又开始继续执行。
这个就像时间禁止的游戏一样,国王对巫师说“我必须马上得到宝物,不然就砍了你的脑袋”,巫师念了一句时间停止的咒语,直到过了1年后勇士们才把宝物送来。这时候巫师解开咒语,把宝物交给国王。这里国王就可以理解成协程,他根本没感觉到时间停止,在他停止到醒来期间发生了什么他不知道,也不关心。
这就是协程的本质。协程是异步非阻塞的另外一种展现形式。Golang,Erlang,Lua协程都是这个模型。
转载地址:http://rango.swoole.com/archives/381
这里单单看这一段可能无法理解,建议去看下这篇转载地址的全文;这里其实说的就是协程对于异步的应用场景;
PHP的协程
PHP实现协程,简单点说是通过yield关键字,准确点说是通过生成器generator类来实现的;
看PHP里面通过generator实现的释放控制:
例子1
function task()
{
echo "生成器 内 第一代码段" . PHP_EOL;
yield;
echo "生成器 内 第二代码段" . PHP_EOL;
yield;
echo "生成器 内 第三代码段" . PHP_EOL;
}
$task = task(); // 初始化生成器
$task->current(); // 执行到第一个yield
$task->next(); // 执行到第二个yield
$task->next(); // 执行第二个yield之后的代码段
/**
* 输出:
* 生成器 内 第一代码段
* 生成器 内 第二代码段
* 生成器 内 第三代码段
*/
这个非常就简单的例子里面,task函数里面的代码的执行是可控的,什么时候中断,什么时候执行,是由生成器控制的;而这个中断,就是释放控制权,代码可以继续执行下去;
Generator生成器可以参考http://blog.csdn.net/alexander_phper/article/details/78523876;
再看一个例子:
例子2
function task()
{
// 这里执行的代码称为生成器内
echo "生成器 内 第一代码段" . PHP_EOL;
yield;
echo "生成器 内 第二代码段" . PHP_EOL;
yield;
echo "生成器 内 第三代码段" . PHP_EOL;
}
// 这里执行的代码称为生成器外
$task = task(); // 初始化生成器
echo "生成器 外 第一代码段" . PHP_EOL;
$task->current();
echo "生成器 外 第二代码段" . PHP_EOL;
$task->next();
echo "生成器 外 第三代码段" . PHP_EOL;
$task->next();
echo "生成器 外 第四代码段" . PHP_EOL;
/**
* 输出:
* 生成器 外 第一代码段
* 生成器 内 第一代码段
* 生成器 外 第二代码段
* 生成器 内 第二代码段
* 生成器 外 第三代码段
* 生成器 内 第三代码段
* 生成器 外 第四代码段
*/
生成器内外的代码在交替执行着;当运行到生成器里面的yield关键字的时候,程序会释放控制权,执行别的程序;
PHP协程实现多任务交替执行
生成器可以中断执行,将控制权交出去,那我们可以让程序在多个生成器中间切换;
例子3
function task1()
{
echo "生成器1 内 第一代码段" . PHP_EOL;
yield;
echo "生成器1 内 第二代码段" . PHP_EOL;
yield;
echo "生成器1 内 第三代码段" . PHP_EOL;
}
function task2()
{
echo "生成器2 内 第一代码段" . PHP_EOL;
yield;
echo "生成器2 内 第二代码段" . PHP_EOL;
yield;
echo "生成器2 内 第三代码段" . PHP_EOL;
}
$task1 = task1(); // 初始化生成器
$task2 = task2();
$task1->current();
$task2->current();
$task1->next();
$task2->next();
$task1->next();
$task2->next();
/**
* 输出:
* 生成器1 内 第一代码段
* 生成器2 内 第一代码段
* 生成器1 内 第二代码段
* 生成器2 内 第二代码段
* 生成器1 内 第三代码段
* 生成器2 内 第三代码段
*/
现在看起来我们可以在两个函数中切换着执行(其实是两个生成器中切换着执行);
这种切换任务的实现方法非常低级,需要手动执行生成器;我们看那一篇原文中的实现方式;
(转载地址:http://nikic.github.io/2012/12/22/Cooperative-multitasking-using-coroutines-in-PHP.html)
(翻译版地址:http://www.laruence.com/2015/05/28/3038.html);
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);
}
}
}
}
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();
}
}
function task1() {
for ($i = 1; $i <= 10; ++$i) {
echo "This is task 1 iteration $i.\n";
yield;
}
}
function task2() {
for ($i = 1; $i <= 5; ++$i) {
echo "This is task 2 iteration $i.\n";
yield;
}
}
$scheduler = new Scheduler;
$scheduler->newTask(task1());
$scheduler->newTask(task2());
$scheduler->run();
/**
* 输出:
* This is task 1 iteration 1.
* This is task 2 iteration 1.
* This is task 1 iteration 2.
* This is task 2 iteration 2.
* This is task 1 iteration 3.
* This is task 2 iteration 3.
* This is task 1 iteration 4.
* This is task 2 iteration 4.
* This is task 1 iteration 5.
* This is task 2 iteration 5.
* This is task 1 iteration 6.
* This is task 1 iteration 7.
* This is task 1 iteration 8.
* This is task 1 iteration 9.
* This is task 1 iteration 10.
*/
这里面引入的调度器类Scheduler和任务类Task(一个任务类可以简单理解就是一个生成器generator),调度器依次控制所有任务执行一次,任务执行一次就是运行到generator的一个yield;所以调度器可以依次执行task1和task2中的代码;当task2中的代码运行完毕之后,继续运行task1中的代码,直到task1中的代码也运行完毕;
现在应该有一个疑问,协程可以干嘛呢?
上面协程那一篇原文中有socket非阻塞IO的应用,可查看鸟哥的翻译版;
不过协程配合异步是个非常好的应用场景;可以查看韩天峰上面的说法;本人也是经过swoole的异步才接触到协程这个概念,现在已经用了很久,但是对协程却很模糊,这才有了这一篇文章;下一篇让我们看下通过携程堆栈实现异步回调,把异步的callback写法直接变成同步的写法;