💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
# 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. ```