专栏名称: SegmentFault思否
SegmentFault (www.sf.gg)开发者社区,是中国年轻开发者喜爱的极客社区,我们为开发者提供最纯粹的技术交流和分享平台。
目录
相关文章推荐
程序猿  ·  41岁DeepMind天才科学家去世:长期受 ... ·  2 天前  
程序员的那些事  ·  清华大学:DeepSeek + ... ·  3 天前  
程序员小灰  ·  清华大学《DeepSeek学习手册》(全5册) ·  3 天前  
程序员小灰  ·  3个令人惊艳的DeepSeek项目,诞生了! ·  2 天前  
程序猿  ·  “我真的受够了Ubuntu!” ·  4 天前  
51好读  ›  专栏  ›  SegmentFault思否

PHP7 下的协程实现

SegmentFault思否  · 公众号  · 程序员  · 2017-12-24 08:00

正文

前言

相信大家都听说过『协程』这个概念吧。

但是有些同学对这个概念似懂非懂,不知道怎么实现,怎么用,用在哪,甚至有些人认为yield就是协程!

我始终相信,如果你无法准确地表达出一个知识点的话,我可以认为你就是不懂。

如果你之前了解过利用PHP实现协程的话,你肯定看过鸟哥的那篇文章:在PHP中使用协程实现多任务调度| 风雪之隅

鸟哥这篇文章是从国外的作者翻译来的,翻译的简洁明了,也给出了具体的例子了。

我写这篇文章的目的,是想对鸟哥文章做更加充足的补充,毕竟有部分同学的基础还是不够好,看得也是云头雾里的。

我个人,不喜欢写长篇文章,微博关注我 @码云 ,每天用微博分享知识。文章同时记录在我的博客:https://bruceit.com/p/A4kSfE

什么是协程

先搞清楚,什么是协程。

你可能已经听过『进程』和『线程』这两个概念。

进程就是二进制可执行文件在计算机内存里的一个运行实例,就好比你的.exe文件是个类,进程就是new出来的那个实例。

进程是计算机系统进行资源分配和调度的基本单位(调度单位这里别纠结线程进程的),每个CPU下同一时刻只能处理一个进程。

所谓的并行,只不过是看起来并行,CPU事实上在用很快的速度切换不同的进程。

进程的切换需要进行系统调用,CPU要保存当前进程的各个信息,同时还会使CPUCache被废掉。

所以进程切换不到非不得已就不做。

那么怎么实现『进程切换不到非不得已就不做』呢?

首先进程被切换的条件是:进程执行完毕、分配给进程的CPU时间片结束,系统发生中断需要处理,或者进程等待必要的资源(进程阻塞)等。你想下,前面几种情况自然没有什么话可说,但是如果是在阻塞等待,是不是就浪费了。

其实阻塞的话我们的程序还有其他可执行的地方可以执行,不一定要傻傻的等!

所以就有了线程。

线程简单理解就是一个『微进程』,专门跑一个函数(逻辑流)。

所以我们就可以在编写程序的过程中将可以同时运行的函数用线程来体现了。

线程有两种类型,一种是由内核来管理和调度。

我们说,只要涉及需要内核参与管理调度的,代价都是很大的。这种线程其实也就解决了当一个进程中,某个正在执行的线程遇到阻塞,我们可以调度另外一个可运行的线程来跑,但是还是在同一个进程里,所以没有了进程切换。

还有另外一种线程,他的调度是由程序员自己写程序来管理的,对内核来说不可见。这种线程叫做『用户空间线程』。

协程可以理解就是一种用户空间线程。

协程,有几个特点:

  • 协同,因为是由程序员自己写的调度策略,其通过协作而不是抢占来进行切换

  • 在用户态完成创建,切换和销毁

  • * ⚠️ 从编程角度上看,协程的思想本质上就是控制流的主动让出(yield)和恢复(resume)机制*

  • generator经常用来实现协程

说到这里,你应该明白协程的基本概念了吧?

PHP实现协程

一步一步来,从解释概念说起!

可迭代对象

PHP5提供了一种定义对象的方法使其可以通过单元列表来遍历,例如用 foreach 语句。

你如果要实现一个可迭代对象,你就要实现 Iterator 接口:

  1. class MyIterator implements Iterator

  2. {

  3.    private $var = array();

  4.    public function __construct($array)

  5.    {

  6.        if (is_array($array)) {

  7.            $this-> var = $array;

  8.        }

  9.    }

  10.    public function rewind() {

  11.        echo "rewinding\n";

  12.        reset($this->var);

  13.    }

  14.    public function current() {

  15.        $var = current($this->var);

  16.        echo "current: $var\n";

  17.        return $var;

  18.    }

  19.    public function key() {

  20.        $var = key($this->var);

  21.        echo "key: $var\n";

  22.        return $var;

  23.    }

  24.    public function next() {

  25.        $var = next($this->var);

  26.        echo "next: $var\n";

  27.        return $var;

  28.    }

  29.    public function valid() {

  30.        $var = $this->current() !== false;

  31.        echo "valid: {$var}\n";

  32.        return $var;

  33.    }

  34. }

  35. $values = array(1,2,3);

  36. $it = new MyIterator($values);

  37. foreach ($it as $a => $b) {

  38.    print "$a: $b\n";

  39. }

生成器

可以说之前为了拥有一个能够被 foreach 遍历的对象,你不得不去实现一堆的方法, yield 关键字就是为了简化这个过程。

生成器提供了一种更容易的方法来实现简单的对象迭代,相比较定义类实现 Iterator 接口的方式,性能开销和复杂性大大降低。

  1. function xrange($start, $end, $step = 1) {

  2.    for ($i = $start; $i <= $end; $i += $step) {

  3.        yield $i;

  4.    }

  5. }

  6. foreach (xrange(1, 1000000) as $num) {

  7.    echo $num, "\n";

  8. }

记住,一个函数中如果用了 yield ,他就是一个生成器,直接调用他是没有用的,不能等同于一个函数那样去执行!

所以, yield 就是 yield ,下次谁再说 yield 是协程,我肯定把你xxxx。

PHP协程

前面介绍协程的时候说了,协程需要程序员自己去编写调度机制,下面我们来看这个机制怎么写。

0)生成器正确使用

既然生成器不能像函数一样直接调用,那么怎么才能调用呢?

方法如下:

  1. foreach他

  2. send($value)

  3. current / next...

1)Task实现

Task就是一个任务的抽象,刚刚我们说了协程就是用户空间协程,线程可以理解就是跑一个函数。

所以Task的构造函数中就是接收一个闭包函数,我们命名为 coroutine

  1. /**

  2. * Task任务类

  3. */

  4. class Task

  5. {

  6.     protected $taskId;

  7.    protected $coroutine;

  8.    protected $beforeFirstYield = true;

  9.    protected $sendValue;

  10.    /**

  11.     * Task constructor.

  12.     * @param $taskId

  13.     * @param Generator $coroutine

  14.     */

  15.    public function __construct($taskId, Generator $coroutine)

  16.    {

  17.        $this->taskId = $taskId;

  18.        $this->coroutine = $coroutine;

  19.    }

  20.    /**

  21.     * 获取当前的Task的ID

  22.     *

  23.     * @return mixed

  24.     */

  25.    public function getTaskId()

  26.    {

  27.        return $this->taskId;

  28.    }

  29.    /**

  30.     * 判断Task执行完毕了没有

  31.     *

  32.     * @return bool

  33.     */

  34.    public function isFinished()

  35.    {

  36.        return !$this->coroutine->valid();

  37.    }

  38.    /**

  39.     * 设置下次要传给协程的值,比如 $id = (yield $xxxx),这个值就给了$id了

  40.     *

  41.     * @param $value

  42.     */

  43.    public function setSendValue($value)

  44.    {

  45.        $this->sendValue = $value;

  46.    }

  47.    /**

  48.     * 运行任务

  49.     *

  50.     * @return mixed

  51.     */

  52.    public function run()

  53.    {

  54.        // 这里要注意,生成器的开始会reset,所以第一个值要用current获取

  55.        if ($this->beforeFirstYield) {

  56.            $this->beforeFirstYield = false;

  57.            return $this->coroutine->current();

  58.        } else {

  59.            // 我们说过了,用send去调用一个生成器

  60.            $retval = $this->coroutine->send($this->sendValue);

  61.            $this->sendValue = null;

  62.            return $retval;

  63.        }

  64.    }

  65. }

2)Scheduler实现

接下来就是 Scheduler 这个重点核心部分,他扮演着调度员的角色。

  1. /**

  2. * Class Scheduler

  3. */

  4. Class Scheduler

  5. {

  6.    /**

  7.     * @var SplQueue

  8.     */

  9.    protected $taskQueue;

  10.    /**

  11.     * @var int

  12.     */

  13.    protected $tid = 0;

  14.    /**

  15.     * Scheduler constructor.

  16.     */

  17.    public function __construct()

  18.    {

  19.        /* 原理就是维护了一个队列,

  20.         * 前面说过,从编程角度上看,协程的思想本质上就是控制流的主动让出(yield)和恢复(resume)机制

  21.         * */

  22.        $this->taskQueue = new SplQueue();

  23.    }

  24.    /**

  25.     * 增加一个任务

  26.     *

  27.     * @param Generator $task

  28.     * @return int

  29.     */

  30.    public function addTask(Generator $task)

  31.    {

  32.        $tid = $this->tid;

  33.        $task = new Task($tid, $task);

  34.        $this->taskQueue->enqueue($task);

  35.        $this->tid++;

  36.        return $tid;

  37.    }

  38.    /**

  39.     * 把任务进入队列

  40.     *

  41.     * @param Task $task

  42.     */

  43.    public function schedule(Task $task)

  44.    {

  45.        $this->taskQueue->enqueue($task);

  46.    }

  47.    /**

  48.     * 运行调度器

  49.     */

  50.    public function run()

  51.    {

  52.        while (!$this->taskQueue->isEmpty()) {

  53.            // 任务出队

  54.            $task = $this->taskQueue->dequeue();

  55.            $res = $task->run(); // 运行任务直到 yield

  56.            if (!$task->isFinished()) {

  57.                $this->schedule($task); // 任务如果还没完全执行完毕,入队等下次执行

  58.            }

  59.        }

  60.    }

  61. }

这样我们基本就实现了一个协程调度器。

你可以使用下面的代码来测试:

  1. function task1() {

  2.    for ($i = 1; $i <= 10; ++$i) {

  3.        echo "This is task 1 iteration $i.\n";

  4.        yield; // 主动让出CPU的执行权

  5.    }

  6. }

  7. function task2() {

  8.    for ($i = 1; $i <= 5; ++$i) {

  9.        echo "This is task 2 iteration $i.\n";

  10.         yield; // 主动让出CPU的执行权

  11.    }

  12. }

  13. $scheduler = new Scheduler; // 实例化一个调度器

  14. $scheduler->addTask(task1()); // 添加不同的闭包函数作为任务

  15. $scheduler->addTask(task2());

  16. $scheduler->run();

关键说下在哪里能用得到PHP协程。

  1. function task1() {

  2.        /* 这里有一个远程任务,需要耗时10s,可能是一个远程机器抓取分析远程网址的任务,我们只要提交最后去远程机器拿结果就行了 */

  3.        remote_task_commit();

  4.        // 这时候请求发出后,我们不要在这里等,主动让出CPU的执行权给task2运行,他不依赖这个结果

  5.        yield;

  6.        yield (remote_task_receive());

  7.        ...

  8. }

  9. function task2() {

  10.    for ($i = 1; $i <= 5; ++$i) {

  11.        echo "This is task 2 iteration $i.\n";

  12.        yield; // 主动让出CPU的执行权

  13.    }

  14. }

这样就提高了程序的执行效率。

关于『系统调用』的实现,鸟哥已经讲得很明白,我这里不再说明。

3)协程堆栈

鸟哥文中还有一个协程堆栈的例子。

我们上面说过了,如果在函数中使用了 yield ,就不能当做函数使用。

所以你在一个协程函数中嵌套另外一个协程函数:

  1. function echoTimes($msg, $max) {

  2.    for ($i = 1; $i <= $max; ++$i) {

  3.        echo "$msg iteration $i\n";

  4.        yield;

  5.    }

  6. }

  7. function task() {

  8.    echoTimes('foo', 10); // print foo ten times

  9.    echo "---\n";

  10.    echoTimes('bar', 5); // print bar five times

  11.    yield; // force it to be a coroutine

  12. }

  13. $scheduler = new Scheduler;

  14. $scheduler->addTask(task());

  15. $scheduler->run();

这里的echoTimes是执行不了的!所以就需要协程堆栈。

不过没关系,我们改一改我们刚刚的代码。

把Task中的初始化方法改下,因为我们在运行一个Task的时候,我们要分析出他包含了哪些子协程,然后将子协程用一个堆栈保存。(C语言学的好的同学自然能理解这里,不理解的同学我建议去了解下进程的内存模型是怎么处理函数调用)

  1. /**

  2.     * Task constructor.

  3.     * @param $taskId

  4.     * @param Generator $coroutine

  5.     */

  6.    public function __construct($taskId, Generator $coroutine)

  7.    {

  8.        $this->taskId = $taskId;

  9.        // $this->coroutine = $coroutine;

  10.        // 换成这个,实际Task->run的就是stackedCoroutine这个函数,不是$coroutine保存的闭包函数了

  11.        $this->coroutine = stackedCoroutine($coroutine);







请到「今天看啥」查看全文