# 2.3 协程原理

## 协程

### 基本概念

“协程”（Coroutine）概念最早由 Melvin Conway 于1958年提出。协程可以理解为纯用户态的线程，其通过协作而不是抢占来进行切换。相对于进程或者线程，协程所有的操作都可以在用户态完成，创建和切换的消耗更低。总的来说，协程为协同任务提供了一种运行时抽象，这种抽象非常适合于协同多任务调度和数据流处理。在现代操作系统和编程语言中，因为用户态线程切换代价比内核态线程小，协程成为了一种轻量级的多任务模型。

从编程角度上看，协程的思想本质上就是控制流的主动让出（yield）和恢复（resume）机制，迭代器常被用来实现协程，所以大部分的语言实现的协程中都有yield关键字，比如Python、PHP、Lua。但也有特殊比如Go就使用的是通道来通信。

### 协程与进程线程的区别

* 对于操作系统来说只有进程和线程，协程的控制由应用程序显式调度，非抢占式的
* 协程的执行最终靠的还是线程，应用程序来调度协程选择合适的线程来获取执行权
* 切换非常快，成本低。一般占用栈大小远小于线程（协程KB级别，线程MB级别），所以可以开更多的协程
* 协程比线程更轻量级

### PHP与协程

PHP从5.5引入了yield关键字，增加了迭代生成器和协程的支持，但并未在语言本身级别实现一个完善的协程解决方案。PHP协程也是基于Generator，Generator可以视为一种“可中断”的函数，而 yield 构成了一系列的“中断点”。PHP 协程没有resume关键字，而是“在使用的时候唤起”协程。了解如何在PHP中实现协程，首先要解决迭代生成器。

```php
function xrange($start, $end, $step = 1) {
    for ($i = $start; $i <= $end; $i += $step) {
        yield $i;
    }
}

foreach (xrange(1, 1000000) as $num) { // xrange返回的是一个Generator对象
    echo $num, "\n";
}
```

具体参考

[PHP > 手册 > 语言参考 > 生成器](http://php.net/manual/zh/language.generators.overview.php)

### 中断点

我们从生成器认识协程，需要认识到：生成器是一种具有中断点的函数，而yield构成了中断点。比如, 你调用$range->rewind()，那么xrange()里的代码就会运行到控制流第一次出现yield的地方，而函数内传递给yield语句的值，即为迭代的当前值，可以通过$xrange->current()获取。

### PHP中的协程实现

PHP的协程支持是在迭代生成器的基础上，增加了可以回送数据给生成器的功能，从而达到双向通信即：

生成器<---数据--->调用者

#### yield接收与发送数据

```php
function gen() {
    $ret = (yield 'yield1');
    var_dump($ret);
    $ret = (yield 'yield2');
    var_dump($ret);
}

$gen = gen();
var_dump($gen->current());     // string(6) "yield1"
var_dump($gen->send('ret1'));  // string(4) "ret1"   (the first var_dump in gen)
		               // string(6) "yield2" (the var_dump of the ->send() return value)
var_dump($gen->send('ret2'));  // string(4) "ret2"   (again from within gen)
				// NULL (the return value of ->send())
```

```php
function gen() {
    yield 'foo';
    yield 'bar';
}

$gen = gen();
var_dump($gen->send('something'));

// 在send之前当$gen迭代器被创建的时候一个renwind()方法已经被隐式调用
// 所以实际上发生的应该类似:
//$gen->rewind();
//var_dump($gen->send('something'));

//这样renwind的执行将会导致第一个yield被执行, 并且忽略了他的返回值.
//真正当我们调用yield的时候, 我们得到的是第二个yield的值! 导致第一个yield的值被忽略.
//string(3) "bar"
```

#### 协程与任务调度

yield指令提供了任务中断自身的一种方法，然后把控制交回给任务调度器。而PHP语言本身只是提供程序中断的功能，至于任务调度器需要我们自己实现，同时协程在运行多个其他任务时，yield还可以用来在任务和调度器之间进行通信。

#### PHP协程任务

简单的定义具有任何ID标识的协程函数，如一个轻量级的协程函数示例代码：
```php
<?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();
    }
}
```
#### PHP协程调度器

简单来说，是可以在多个任务之间相互协调，及任务之间相互切换的一种进程资源的分配器。调度器的实现方式有多种，大致分为两类：一是，队列；二是，定时器。

```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);
            }
        }
    }
}
```
newTask()方法创建一个新任务，然后把这个任务放入任务map数组里，接着它通过把任务放入任务队列里来实现对任务的调度。接着run()方法扫描任务队列，运行任务，如果一个任务结束了，那么它将从队列里删除，否则它将在队列的末尾再次被调度。

协程示例：

```php
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.
```

# links
  * [目录](../README.md)
  * 上一节: [swoole](2.2-swoole.md)
  * 下一节: [异步、并发](2.4-异步、并发.md)