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中实现协程,首先要解决迭代生成器。

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 > 手册 > 语言参考 > 生成器

中断点

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

PHP中的协程实现

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

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

yield接收与发送数据

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())
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
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协程调度器

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

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()方法扫描任务队列,运行任务,如果一个任务结束了,那么它将从队列里删除,否则它将在队列的末尾再次被调度。

协程示例:

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

results matching ""

    No results matching ""