php的yield是在php5.5版本就出来了,而在初级php界却很少有人提起,我就说说个人对php yield的理解

Iterator接口

在php中,除了数组,对象可以被foreach遍历之外,还有另外一种特殊对象,也就是继承了iterator接口的对象,也可以被对象遍历,但和普通对象的遍历又有所不同,下面是3种类型的遍历情况:


可以看出,迭代器的遍历,会依次调用重置,检查当前数据,返回当前指针数据,指针下移方法,结束遍历的条件在于检查数据返回true或者false

生成器

生成器和迭代器类似,但也完全不同

生成器允许你在 foreach 代码块中写代码来迭代一组数据而不需要在内存中创建一个数组, 那会使你的内存达到上限,或者会占据可观的处理时间。相反,你可以写一个生成器函数,就像一个普通的自定义函数一样, 和普通函数只返回一次不同的是, 生成器可以根据需要 yield 多次,以便生成需要迭代的值。

生成器使用yield关键字进行生成迭代的值

例如:

一:生成器方法

生成器它的内部实现了以下方法:

Generator implements Iterator {    //返回当前产生的值
   public mixed current ( void )    //返回当前产生的键
   public mixed key ( void )    //生成器继续执行
   public void next ( void )    //重置迭代器,如果迭代已经开始了,这里会抛出一个异常。
   public void rewind ( void )    //向生成器中传入一个值,当前yield接收值,然后继续执行下一个yield
   public mixed send ( mixed $value )    //向生成器中抛入一个异常
   public void throw ( Exception $exception )    //检查迭代器是否被关闭,已被关闭返回 FALSE,否则返回 TRUE
   public bool valid ( void )    //序列化回调
   public void __wakeup ( void )    //返回generator函数的返回值,PHP version 7+
   public mixed getReturn ( void )
}

二:语法

生成器的语法有很多种用法,需要一一说明,首先,yield必须有函数包裹,包裹yield的函数称为"生成器函数",该函数将返回一个可遍历的对象

1:颠覆常识的yield

可能你在这发现了几个东西,和之前php完全不同的认知,如果你没发现,额,那我提出来吧

1:在调用函数返回的时候,可以发现for里面的语句并没有执行

2:在遍历一次的时候,可以发现调用函数,却没有正常的for循环3次,只循环了一次

3:在遍历一次的情况时,"存在感2"竟然没有调用,在一直遍历的情况下才调用

再看看另一个例子:

什么????while(ture)竟然还能正常的执行下去???没错,生成器函数就是这样的,根据这个例子,我们发现了这些东西:

1:while(true)没有阻塞调用函数下面的代码执行,却导致了下面的echo "额额额"和return 无法执行

2:return 返回值竟然是没有作用的

3:send(1)时,没有echo "哈哈",send(2)时,才开始出现"哈哈",

2:yield的其他语法

yield表达式中,也可以赋值,但赋值需要使用括号包裹:

只需要在表达式后面加上$key=>$value,即可生成键值的数据:

在函数前增加引用定义,就可以像returning references from functions(从函数返回一个引用)一样 引用生成值

三:特性总结

1:yield是生成器所需要的关键字,必须在函数内部,有yield的函数叫做"生成器函数"

2:调用生成器函数时,函数将返回一个继承了Iterator的生成器

3:yield作为表达式使用时,可将一个值加入到生成器中进行遍历,遍历完会中断下面的语句运行,并且保存状态,当下次遍历时会继续执行(这就是while(true)没有造成阻塞的原因)

4:当send传入参数时,yield可作为一个变量使用,这个变量等于传入的参数

协程

一:实现个简单的协程

协程,是一种编程逻辑的转变,使多个任务能交替运行,而不是之前的一直根据流程往下走,举个例子

当有一个逻辑,每次调用这个文件时,该文件要做3件事:

1:写入300个文件

2:发送邮件给500个会员

3:插入100条数据

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php
function task1(){
    for ($i=0;$i<=300;$i++){
        //写入文件,大概要3000微秒
        usleep(3000);
        echo "写入文件{$i}\n";
    }
}
function task2(){
    for ($i=0;$i<=500;$i++){
        //发送邮件给500名会员,大概3000微秒
        usleep(3000);
        echo "发送邮件{$i}\n";
    }
}
function task3(){
    for ($i=0;$i<=100;$i++){
        //模拟插入100条数据,大概3000微秒
        usleep(3000);
        echo "插入数据{$i}\n";
    }
}
task1();
task2();
task3();

这样,就实现了这3个功能了,然而,技术组长又说:

能不能改成交替运行呢?

就是说,写入文件一次之后,马上去发送一次邮件,然后再去插入一条数据

然后我改一改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<?php
function task1($i)
{
    //使用$i标识 写入文件,大概要3000微秒
    if ($i > 300) {
        return false;//超过300不用写了
    }
    echo "写入文件{$i}\n";
    usleep(3000);
    return true;
}
 
function task2($i)
{
    //使用$i标识 发送邮件,大概要3000微秒
    if ($i > 500) {
        return false;//超过500不用发送了
    }
    echo "发送邮件{$i}\n";
    usleep(3000);
    return true;
}
 
function task3($i)
{
    //使用$i标识 插入数据,大概要3000微秒
    if ($i > 100) {
        return false;//超过100不用插入
    }
    echo "插入数据{$i}\n";
    usleep(3000);
    return true;
}
 
$i           = 0;
$task1Result = true;
$task2Result = true;
$task3Result = true;
while (true) {
    $task1Result && $task1Result = task1($i);
    $task2Result && $task2Result = task2($i);
    $task3Result && $task3Result = task3($i);
    if($task1Result===false&&$task2Result===false&&$task3Result===false){
        break;//全部任务完成,退出循环
    }
    $i++;
}

运行一下:

代码1:

代码2:

确实是实现了任务交替执行,但是代码2明显让代码变的非常的难读,扩展性也很差,那么,有没有更好的方式实现这个功能呢?

这时候我们就必须借助yield了

首先,我们得封装一个任务类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/**
 * 任务对象
 * Class Task
 */
class Task {
    protected $taskId;//任务id
    protected $coroutine;//生成器
    protected $sendValue = null;//生成器send值
    protected $beforeFirstYield = true;//迭代指针是否是第一个
 
    public function __construct($taskId, Generator $coroutine) {
        $this->taskId = $taskId;
        $this->coroutine = $coroutine;
    }
 
    public function getTaskId() {
        return $this->taskId;
    }
 
    /**
     * 设置插入数据
     * @param $sendValue
     */
    public function setSendValue($sendValue) {
        $this->sendValue = $sendValue;
    }
 
    /**
     * send数据进行迭代
     * @return mixed
     */
    public function run() {
        //如果是
        if ($this->beforeFirstYield) {
            $this->beforeFirstYield = false;
            var_dump($this->coroutine->current());
            return $this->coroutine->current();
        else {
            $retval $this->coroutine->send($this->sendValue);
            $this->sendValue = null;
            return $retval;
        }
    }
 
    /**
     * 是否完成
     * @return bool
     */
    public function isFinished() {
        return !$this->coroutine->valid();
    }
}

这个封装类,可以更好的去调用运行生成器函数,但只有这个也是不够的,我们还需要一个调度任务类,来代替前面的while:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
 * 任务调度
 * Class Scheduler
 */
class Scheduler {
    protected $maxTaskId = 0;//任务id
    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;
    }
 
    /**
     * 任务入列
     * @param Task $task
     */
    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);
            }
        }
    }
}

很好,我们已经有了一个调度类,还有了一个任务类,可以继续实现上面的功能了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
function task1()
{
    for ($i = 0; $i <= 300; $i++) {
        //写入文件,大概要3000微秒
        usleep(3000);
        echo "写入文件{$i}\n";
        yield $i;
    }
}
 
function task2()
{
    for ($i = 0; $i <= 500; $i++) {
        //发送邮件给500名会员,大概3000微秒
        usleep(3000);
        echo "发送邮件{$i}\n";
        yield $i;
    }
}
 
function task3()
{
    for ($i = 0; $i <= 100; $i++) {
        //模拟插入100条数据,大概3000微秒
        usleep(3000);
        echo "插入数据{$i}\n";
        yield $i;
    }
}
 
$scheduler new Scheduler;
 
$scheduler->newTask(task1());
$scheduler->newTask(task2());
$scheduler->newTask(task3());
 
$scheduler->run();

除了上面的2个类,task函数和代码1不同的地方,就是多了个yield,那我们试着运行一下:

很好,我们已经实现了可以调度任务,进行任务交叉运行的功能了,这就是"协程"

协程可以将多个不同的任务交叉运行

二:协程与调度器的通信

我们在上面已经实现了一个协程封装了,但是任务和调度器缺少了通信,我们可以重新封装下,使协程当中能够获取当前的任务id,新增任务,以及杀死任务

先封装一下调用的封装:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class YieldCall
{
    protected $callback;
 
    public function __construct(callable $callback)
    {
        $this->callback = $callback;
    }
 
    /**
     * 调用时将返回结果
     * @param Task $task
     * @param Scheduler $scheduler
     * @return mixed
     */
    public function __invoke(Task $task, Scheduler $scheduler)
    {
        $callback $this->callback;
        return $callback($task$scheduler);
    }
}

同时我们需要小小的改动下调度器的run方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public function run()
{
    while (!$this->taskQueue->isEmpty()) {
        $task   $this->taskQueue->dequeue();
        $retval $task->run();
 
        //如果返回的是YieldCall实例,则先执行
        if ($retval instanceof YieldCall) {
            $retval($task$this);
            continue;
        }
        if ($task->isFinished()) {
            unset($this->taskMap[$task->getTaskId()]);
        else {
            $this->schedule($task);
        }
    }
}

新增 getTaskId函数去返回task_id:

1
2
3
4
5
6
7
8
9
10
11
function getTaskId()
{
    //返回一个YieldCall的实例
    return new YieldCall(
        //该匿名函数会先获取任务id,然后send给生成器,并且由YieldCall将task_id返回给生成器函数
        function (Task $task, Scheduler $scheduler) {
            $task->setSendValue($task->getTaskId());
            $scheduler->schedule($task);
        }
    );
}

然后,我们再修改下task1,task2,task3函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
function task1()
{
    $task_id = (yield getTaskId());
    for ($i = 0; $i <= 300; $i++) {
        //写入文件,大概要3000微秒
        usleep(3000);
        echo "任务{$task_id}写入文件{$i}\n";
        yield $i;
    }
}
 
function task2()
{
    $task_id = (yield getTaskId());
    for ($i = 0; $i <= 500; $i++) {
        //发送邮件给500名会员,大概3000微秒
        usleep(3000);
        echo "任务{$task_id}发送邮件{$i}\n";
        yield $i;
    }
}
 
function task3()
{
    $task_id = (yield getTaskId());
    for ($i = 0; $i <= 100; $i++) {
        //模拟插入100条数据,大概3000微秒
        usleep(3000);
        echo "任务{$task_id}插入数据{$i}\n";
        yield $i;
    }
}
 
$scheduler new Scheduler;
 
$scheduler->newTask(task1());
$scheduler->newTask(task2());
$scheduler->newTask(task3());
 
$scheduler->run();

执行结果:

这样的话,当第一次执行的时候,会先调用getTaskId将task_id返回,然后将任务继续执行,这样,我们就获取到了调度器分配给任务的task_id,是不是很神奇?

三:生成新任务以及杀死任务

现在新增了一个需求:当发送邮件给会员时,需要新增一个发送短信的子任务,当会员id大于200时则停止 (别问我为什么要这样做,我自己都不知道)

同时,我们可以利用YieldCall,去新增任务和杀死任务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
 * 传入一个生成器函数用于新增任务给调度器调用
 * @param Generator $coroutine
 * @return YieldCall
 */
function newTask(Generator $coroutine) {
    return new YieldCall(
        //该匿名函数,会在调度器中新增一个任务
        function(Task $task, Scheduler $scheduleruse ($coroutine) {
            $task->setSendValue($scheduler->newTask($coroutine));
            $scheduler->schedule($task);
        }
    );
}
 
/**
 * 杀死一个任务
 * @param $tid
 * @return YieldCall
 */
function killTask($taskId) {
    return new YieldCall(
        //该匿名函数,传入一个任务id,然后让调度器去杀死该任务
        function(Task $task, Scheduler $scheduleruse ($taskId) {
            $task->setSendValue($scheduler->killTask($taskId));
            $scheduler->schedule($task);
        }
    );
}

同时,调度器也得有killTask的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
 * 杀死一个任务
 * @param $taskId
 * @return bool
 */
public function killTask($taskId)
{
    if (!isset($this->taskMap[$taskId])) {
        return false;
    }
 
    unset($this->taskMap[$taskId]);
 
    /**
     * 遍历队列,找出id相同的则删除
     */
    foreach ($this->taskQueue as $i => $task) {
        if ($task->getTaskId() === $taskId) {
            unset($this->taskQueue[$i]);
            break;
        }
    }
 
    return true;
}

有了新增和删除,我们就可以重新写一下task2以及新增task4:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function task4(){
    $task_id = (yield getTaskId());
    while (true) {
        echo "任务{$task_id}发送短信\n";
        yield;
    }
}
function task2()
{
    $task_id = (yield getTaskId());
    $child_task_id = (yield newTask(task4()));
    for ($i = 0; $i <= 500; $i++) {
        //发送邮件给500名会员,大概3000微秒
        usleep(3000);
        echo "任务{$task_id}发送邮件{$i}\n";
        yield $i;
        if($i==200){
           yield killTask($child_task_id);
        }
    }
}

运行结果:

这样我们就完美的实现了新增任务,以及杀死任务了

总结

前面所说的,协程只是一种编程逻辑,一种写代码的技巧,协程能够帮助我们更好的切换代码中任务

从上面的例子不难发现,其实协程实现封装较为麻烦,并且不用协程也能实现这些功能,那为什么要用协程呢?

因为协程可以让代码更加的简洁,任务相互之间独立区分开,可以使代码更加的清爽

协程让我们可以更好的控制切换任务流

前面介绍了那么多,或许有很多人感觉不对,会说"协程不能提升效率吗?","协程到底用来干什么的?"

或许由上面的例子很难看出协程的用处,那我们继续举例子吧:

js ajax是phper都了解的一个技术,

当点击一个按钮时,先将点击事件ajax传输给后端进行增加一条点击数据,然后出现一个动画,这是一个很正常的事,那么请问,如果ajax是同步,并且在网络不好的情况,会发生什么呢?

没错,点击之后,页面将会卡几秒(网络不好),请求完毕之后,才会出现一个动画.

协程的用处就在这了,我们可以利用协程,把一些同步io等待的代码逻辑,改为异步,在等待的时间内,可以让cpu去处理其他任务,

就如同小学时候做的一道题:

小明烧开水需要10分钟,刷牙需要3分钟,吃早餐需要5分钟,请问做完这些事情总共需要多少分钟?

答案是10分钟,因为在烧开水这个步骤时,不需要坐在那里看水壶烧(异步,io耗时)可以先去刷牙,然后去吃早餐

以上就是php yield关于协程的全部内容了

swoole

由总结可以看出,协程用在最多的应用场景,在于需要io耗时,cpu可以节省出来的场景,并且必须要是异步操作

这里推荐swoole扩展https://www.swoole.com/,

Swoole-4.1.0正式版发布, 主要改动及新特性:

+ PHP原生Redis、PDO、MySQLi轻松协程化, 使用Swoole\Runtime::enableCorotuine()即可将普通的同步阻塞Redis、PDO、MySQLi操作变为协程调度的异步非阻塞IO

+ 协程跟踪功能: 新增两个方法 Coroutine::listCoroutines()可遍历当前所有协程, Coroutine::getBackTrace($cid)可获取某个协程的函数调用栈

+ 支持在协程和Server中使用exit, 此时将会抛出可捕获的\Swoole\ExitException异常

+ 移除所有迭代器(table/connection/coroutine_list)的PCRE依赖限制

+ 新增http_compression配置项, 底层会自动判断客户端传入的Accept-Encoding选择合适的压缩方法, 支持GoogleBrotli压缩

+ 重构了底层Channel和协程Http客户端的C代码为C++协程模式, 解决历史遗留的异步时序问题, 稳定性大大提升

+ 更完整稳定的HTTP2支持和SSL处理

+ 增加open_websocket_close_frame配置, 可以在onMessage事件中接收close帧

具体更新内容文档: https://wiki.swoole.com/wiki/page/966.html

php yield关键字以及协程的实现的更多相关文章

  1. 用yield实现python协程

    刚刚介绍了pythonyield关键字,趁热打铁,现在来了解一下yield实现协程. 引用官方的说法: 与线程相比,协程更轻量.一个python线程大概占用8M内存,而一个协程只占用1KB不到内存.协 ...

  2. python yield、yield from与协程

    从生成器到协程 协程是指一个过程,这个过程与调用方协作,产出由调用方提供的值.生成器的调用方可以使用 .send(...)方法发送数据,发送的数据会成为yield表达式的值.因此,生成器可以作为协程使 ...

  3. 00.用 yield 实现 Python 协程

    来源:Python与数据分析 链接: https://mp.weixin.qq.com/s/GrU6C-x4K0WBNPYNJBCrMw 什么是协程 引用官方的说法: 协程是一种用户态的轻量级线程,协 ...

  4. php +go关键字实现协程

    来源: https://studygolang.com/articles/17631?fr=sidebar 今天在知乎浏览时忽然发现了一个有趣的东西,php竟然可以实现协程的实现,而且还是通过go关键 ...

  5. yield实现 coroutine协程案例

    yield可以手工实现协程,但python为我们封装了一个greenlet,先看看yield实现,yield需要手工操作,无法实现IO操作时自动切换协程,greenlet是封装好的,能方便使用io切换 ...

  6. yield支持的协程

    #_author:来童星#date:2019/12/12def consumer(name): print("--->start...") while True: new_b ...

  7. Unity 新手入门 如何理解协程 IEnumerator yield

    Unity 新手入门 如何理解协程 IEnumerator 本文包含两个部分,前半部分是通俗解释一下Unity中的协程,后半部分讲讲C#的IEnumerator迭代器 协程是什么,能干什么? 为了能通 ...

  8. python教程:使用 async 和 await 协程进行并发编程

    python 一直在进行并发编程的优化, 比较熟知的是使用 thread 模块多线程和 multiprocessing 多进程,后来慢慢引入基于 yield 关键字的协程. 而近几个版本,python ...

  9. Python并发实践_02_通过yield实现协程

    python中实现并发的方式有很多种,通过多进程并发可以真正利用多核资源,而多线程并发则实现了进程内资源的共享,然而Python中由于GIL的存在,多线程是没有办法真正实现多核资源的. 对于计算密集型 ...

随机推荐

  1. (解决某些疑难杂症)Ubuntu16.04 + NVIDIA显卡驱动 + cuda10 + cudnn 安装教程

    一.NVIDIA显卡驱动 打开终端,输入: sudo nautilus 在新打开的文件夹中,进入以下路径(不要用命令行): 左下角点计算机,lib,modules 这时会有几个文件夹,对每个文件夹都进 ...

  2. 自己关于SSM框架的搭建

    第一步 导入相应的包 spring springmvc 需要的包 spring-webmvc spring-aop spring-beans apring-context spring-core sp ...

  3. spring的统一进行异常处理

    public class ExceptionHandler extends SimpleMappingExceptionResolver { private static final Logger l ...

  4. PLC 控制系统资源

    之前整理的PC高级语言与PLC通讯代码下载链接:三菱:http://blog.sina.com.cn/s/blog_16d7d3ecb0102x6wj.html倍福:http://bbs.elecfa ...

  5. java之重装系统重新配置环境变量 jdk、eclipse、idea、Oracle、svn、gitlab等环境变量的安装

    前言:由于公司电脑进行统一版本升级,需要重装系统(只对C盘做升级),记录一下踩过的坑! 首先理一下思路,看那些东西需要做: 1.jdk及其环境变量 2.eclipse(文件夹版的需要运行项目进行测试) ...

  6. 【leetcode】543. Diameter of Binary Tree

    题目如下: 解题思路:最长的周长一定是树中某一个节点(不一定是根节点)的左右子树中的两个叶子节点之间的距离,所以最简单的办法就是把树中所有节点的左右子树中最大的两个叶子节点之间的距离求出来,最终得到最 ...

  7. Spring Security 安全认证

    Spring Boot 使用 Mybatis 依赖 <dependency> <groupId>org.mybatis.spring.boot</groupId> ...

  8. springmvc对象作为 目标方法的参数。

    /** * Spring MVC 会按请求参数名和 POJO 属性名进行自动匹配, 自动为该对象填充属性值.支持级联属性. * 如:dept.deptId.dept.address.tel 等 */ ...

  9. 【BZOJ2555】SubString(后缀自动机,LCT)

    题意:给你一个字符串init,要求你支持两个操作 (1):在当前字符串的后面插入一个字符串 (2):询问字符串s在当前字符串中出现了几次?(作为连续子串) 你必须在线支持这些操作. 长度 <= ...

  10. 用DELPHI中实现RAR文件解压到指定一目录

    一个RAR压缩文件,用DELPHI编的程序打开它并解压到某一目录,怎么实现的?自己搞定了例子:winrar.exe e -y C:\WINDOWS\Desktop\ghost.rar d:\ 但新的问 ...