前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >PHP中的yield与协程调度器(二十二节上)

PHP中的yield与协程调度器(二十二节上)

作者头像
老李秀
发布2020-04-07 18:44:13
1.2K0
发布2020-04-07 18:44:13
举报

这是一个全新的时代,崭新的科技日新月异,TA就像疾驰而过的复兴号一闪而过,而你仅仅因为临时下车冒了一袋烟就被永远地甩下!同步阻塞的时代已经远去,异步非阻塞的脚步也早已踏离,迎接我们的是全新时代的并发解决方案 --- 协程!协程来了!TA看见,TA征服,TA血液里充斥着狼性,TA就是铁王座的继承人,TA就是新时代的帝皇...

而对于PHP而言,内置的原生协程的,有的仅仅只有一个叫做yield的关键字,但是这个关键字返回的数据类型实际上叫做「生成器」,你说TA叫协程是不太严格的。在此之前我写了两篇关于PHP yield基础的文章,建议没看的先看下基础语法:

由于这里的概念和使用逻辑可能比较不太容易理解,所以我们还是通过循序渐进的方式来整明白,首先今天这篇文章尝试利用yield来实现一个简单的协程调度器,然后下一篇文章基于这个协程调度器实现一个socket服务器。网上其实关于PHP yield实现协程调度器的资料文章非常少,官方文档除了基础语法外毛都没讲,所以我这里是参考了鸟哥博客上那篇文章还有有赞那个基于swoole的协程框架,在此基础之上按照自己的理解进行了一些整理汇总。

前面我们说过,对于yield而言,TA最重要的作用就是「让当前正在运行的程序让出CPU」,然后当程序再次占据CPU的时候接着从上次停止运行的地方继续运行。下面我们将调度器和协程任务分别抽象成两个class:

代码语言:javascript
复制
<?php
/*
* @desc : 一个任务的抽象,你可以理解为,一个协程
* */
class Task {
    public $i_task_id;
    public $g_coroutine;
    public $m_send_value;
    public $b_is_first_yield = true;
    public function __construct( $i_task_id, Generator $g_coroutine ) {
        $this->g_coroutine = $g_coroutine;
        $this->i_task_id   = $i_task_id;
    }
    public function set_send_value( $m_send_value ) {
        $this->m_send_value = $m_send_value;
    }
    public function get_task_id() {
        return $this->i_task_id;
    }
    public function run() {
        // 如果是第一次执行yield
        // 第一个yield的值要用current方法返回
        if ( true === $this->b_is_first_yield ) {
            $this->b_is_first_yield = false;
            return $this->g_coroutine->current();
        }
        // 只要不是第一次yield,剩下的值都用send双向通道里获取到
        else {
            $m_yield_ret = $this->g_coroutine->send( $this->m_send_value );
            $this->m_send_value = null;
            return $m_yield_ret;
        }
    }
    // 注意这个方法的内在逻辑是这样的
    // 如果说当前的coroutine是可用的,那么就表示「还没有结束」
    // 如果说当前的coroutine是不可用的,那么就表示「已经结束了」
    // 所以,前面要取反,加上!
    public function is_finish() {
        return !$this->g_coroutine->valid();
    }
}

其次是一个粗暴的调度器:

代码语言:javascript
复制
<?php
class Scheduler {
    public $i_current_task_id  = 0;     // 任务管理器当前最大的任务id
    public $a_task_map = array();

    // 创建一个新的调度器,就是初始化一个array用来存储task对象
    public function __construct() {
        $this->a_task_map = array();
    }

    public function new_task( Generator $g_coroutine ) {
        $i_task_id = $this->i_current_task_id++;
        $o_task    = new Task( $i_task_id, $g_coroutine );
        $this->a_task_map[ $i_task_id ] = $o_task;
        return $i_task_id;
    }

    public function run() {
        while ( count( $this->a_task_map ) > 0 ) {
            $o_task = array_shift( $this->a_task_map );
            $o_task->run();
            if ( $o_task->is_finish() ) {
                unset( $this->a_task_map[ $o_task->get_task_id() ] );
            }
            else {
                array_push( $this->a_task_map, $o_task );
            }
        }
    }
}

上面的代码里,有一个地方我要重点描述一下,就是下面这段:

代码语言:javascript
复制
public function run() {
        // 如果是第一次执行yield
        // 第一个yield的值要用current方法返回
        if ( true === $this->b_is_first_yield ) {
            $this->b_is_first_yield = false;
            return $this->g_coroutine->current();
        }
        // 只要不是第一次yield,剩下的值都用send双向通道里获取到
        else {
            $m_yield_ret = $this->g_coroutine->send( $this->m_send_value );
            $this->m_send_value = null;
            return $m_yield_ret;
        }
}

我们先看个小demo片段,你复制粘贴走,然后运行一下:

代码语言:javascript
复制
<?php
function gen() {
    yield 'foo1';
    yield 'foo2';
    yield 'foo3';
}
$gen = gen();
var_dump($gen->send('something'));

怎么样,铁子,和你脑海里预想的结果一致不?如果不一致,嗯,那就是正常水平;如果一致,那TM也是瞎猜的...为啥会出现这个结果呢,这个也没为啥,其实就是当你第一次对生成器执行send方法的时候会执行一次隐形的$gen->rewind(),然后第一个yield 'foo1'会被忽略而直接执行第二个yield 'foo2',如何获得第一个yield 'foo1'的值呢?用$gen->current()即可。

结合上面的结论,然后你再看看Task类的run()方法,明白了不?

然后我们简单使用一下这个携程调度器:

代码语言:javascript
复制
// 不要想太多
// 你就粗暴认为这就是一个协程
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;
    }
}
// 将这两个协程任务添加到调度器中
// 让调度器把这两个协程任务run起来...
$scheduler = new Scheduler();
$scheduler->new_task( task1() );
$scheduler->new_task( task2() );
$scheduler->run();

复制粘贴走跑一下,至于你们那里是啥样我不知道,反正我这里是这样的:

这就是一个非常粗暴简单的协程调度器,今天这篇是正式打开一扇门,上面代码你们好好琢磨琢磨想想,下篇会结合socket会非常恶心,同时:下一篇也将是《PHP网络编程》的最后一章终章,自此我们就彻底说完了PHP网络编程里出现的同步阻塞服务器、异步非阻塞服务器、协程,至于各位有没有收获,已经不在我了,全看诸君自己了。

PHP网络编程写完了,后面开个什么短平快的系列呢?C语言?APUE?手写Redis?接着附近的人?

本文参与?腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-03-29,如有侵权请联系?cloudcommunity@tencent.com 删除

本文分享自 高性能API社区 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与?腾讯云自媒体分享计划? ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
云数据库 Redis
腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
http://www.vxiaotou.com