php yield学习笔记(一)

php yield学习笔记(一)

说明yield关键字的说明网上有很多(文末会附上相关博客链接),这里我只说明我认为最基础的东西。那就是搞明白Iterator方法的调用顺序,以及Iterator方法在foreach中的对应关系。

yield使用介绍

yield实现协程调度

yield使用介绍

# Iterator接口摘要
Iterator extends Traversable {
    /* Methods */
    abstract public current ( ) : mixed
    abstract public key ( ) : scalar
    abstract public next ( ) : void
    abstract public rewind ( ) : void
    abstract public valid ( ) : bool
}

# 当我们试图遍历(foreach)一个Iterator实例的时候,Iteator方法调用顺序如下:
rewind->valid->current->key->
    next->valid->current->key->
    	next->valid->current->key->
    		next->valid... # 直到valid()返回false 停止迭代 请各位牢记此调用顺序
    
# Generator类摘要 对应实例可迭代
Generator implements Iterator {
    /* Methods */
    public current ( ) : mixed
    public key ( ) : mixed
    public next ( ) : void
    public rewind ( ) : void
    public send ( mixed $value ) : mixed
    public throw ( Exception $exception ) : void
    public valid ( ) : bool
    public __wakeup ( ) : void
    public getReturn ( ) : mixed
}
# 生成器的根本执行流程:
#1、外部每调用一次能够使生成器产生“位移”的方法,生成器内部就会执行到下一个yield语句停止,或者在生成器自然结束或者return的地方停止。
#2、当生成器停在一个非yield的地方,非"位移"方法(比如key和current),也会在生成器内部产生位移

# current方法执行到最近的一个yield语句,在获得产出后停止

# next方法在生成器内部会执行代码,会跳过一个完整的yield语句,直到下一个yield前停止

# rewind方法会在返回Generator实例时自动调用,显示调用rewind方法生成器会尝试将代码定位到第一个yield之前,但为什么显式调用rewind方法可能会报错呢,
# 这是因为rewind不允许真正的将代码回滚到执行过的代码段(Cannot rewind a generator that was already run )

# send方法向生成器中传入一个值,作为下次要迭代的yield的值产出
# send方法的返回值是下一个yield的产出。如果没有下一个yield就没有产出,send的返回值自然就是null
# 基于以上send方法可以简单认为在此之后,继续隐式调用了next和current方法(作为返回值)

# getReturn方法只能在迭代完毕后调用,没有返回值就是null

# IteratorAggregate接口摘要   foreach一个实现此接口的类实例时,会自动调用getIterator方法,从而保证迭代
# php内置了很多实现了Traversable的类,比如官方例子中的ArrayIterator,方便我们遍历各种资源,这便是SPL类库的目的,提供通用的解决方案
IteratorAggregate extends Traversable {
    /* Methods */
    abstract public getIterator ( ) : Traversable
}

# 下面直接复制一段php官方文档的代码,我会尝试解释执行流程
<?php
class X implements IteratorAggregate {
    public function getIterator(){
        yield from [1,2,3,4,5];
    }
    public function getGenerator(){
        foreach ($this as $j => $each){
        // foreach ([1, 2, 3, 4, 5] as $j => $each) {
            echo "getGenerator(): yielding: {$j} => {$each}\n";
            $val = (yield $j => $each);
            yield; // ignore foreach's next()
            echo "getGenerator(): received: {$j} => {$val}\n";
        }
    }
}
$x = new X;

foreach ($x as $i => $val){
    echo "getIterator(): {$i} => {$val}\n";
}
echo "\n";

$gen = $x->getGenerator();
foreach ($gen as $j => $val){
    echo "getGenerator(): sending:  {$j} => {$val}\n";
   	// $gen->send($val);
    var_dump($gen->send($val));
}

# 运行结果如下
getIterator(): 0 => 1
getIterator(): 1 => 2
getIterator(): 2 => 3
getIterator(): 3 => 4
getIterator(): 4 => 5

getGenerator(): yielding: 0 => 1
getGenerator(): sending:  0 => 1
NULL
getGenerator(): received: 0 => 1
getGenerator(): yielding: 1 => 2
getGenerator(): sending:  1 => 2
NULL
getGenerator(): received: 1 => 2
getGenerator(): yielding: 2 => 3
getGenerator(): sending:  2 => 3
NULL
getGenerator(): received: 2 => 3
getGenerator(): yielding: 3 => 4
getGenerator(): sending:  3 => 4
NULL
getGenerator(): received: 3 => 4
getGenerator(): yielding: 4 => 5
getGenerator(): sending:  4 => 5
NULL
getGenerator(): received: 4 => 5

# 分析 我们只看第二段代码和对应的getGenerator类方法
$gen = $x->getGenerator();
foreach ($gen as $j => $val){
    echo "getGenerator(): sending:  {$j} => {$val}\n";
    // $gen->send($val);
    var_dump($gen->send($val));
}
# 下面是调用顺序
第一轮:rewind->valid->current->key->getGenerator(): yielding: 0 => 1->
    	getGenerator(): sending:  0 => 1->$gen->send(1)->
    	$val = (yield $j => $each)(此时$val为1)->yield(产出null)->
    	var_dump($gen->send($val)) # null;
至此第一轮结束,获得输出如下:
getGenerator(): yielding: 0 => 1
getGenerator(): sending:  0 => 1
NULL
    
第二轮:next(生成器内部跳过本次yield,运行至下一个yield为止,中途会输出
		getGenerator(): received: 0 => 1 因为val是上次send进来的1)->
    	valid->current->key->getGenerator(): yielding: 1 => 2->
    	getGenerator(): sending:  1 => 2->$gen->send(2)->
    	$val = (yield $j => $each)(此时$val为2)->yield(产出null)->
 		var_dump($gen->send($val)) # null;
至此第二轮结束,获得输出如下:
getGenerator(): received: 0 => 1
getGenerator(): yielding: 1 => 2
getGenerator(): sending:  1 => 2
NULL
... 
直到->valid返回false 迭代结束

# 各位也可以使用Iterator方法单步调试代码,以便更直观的感受任务的切换和调度

yield协程调度实现原理

# 这里介绍鸟哥那片著名博客提供的思路
一个任务就是一个协程,需要使用不同的任务id进行区分。想要让调度器调度任务就需要先将相应任务注册到调度器,实际上保存到了调度器内部的队列中,
当队列不为空的时候就会循环执行调度这些任务。由于是循环执行任务,想要将任务从非执行时转为执行时(即调度器将执行权分配给指定的任务),需要通过send方法实现调度。
当任务获取执行权后,执行到下一个yield会丢失执行时(即将执行权归还给调度器),调度器会判断刚刚执行的任务是否执行完毕,如果没执行完就将任务重新投入队列,
让任务等待下一次执行权的分配。
    
# 我们直接上鸟哥提供的第三个例子
<?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();
    }
}

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();
            $retval = $task->run();
            if ($retval instanceof SystemCall) {
                $retval($task, $this);
                continue;
            }
            if ($task->isFinished()) {
                unset($this->taskMap[$task->getTaskId()]);
            } else {
                $this->schedule($task);
            }
        }
    }

    public function killTask($tid)
    {
        if (!isset($this->taskMap[$tid])) {
            return false;
        }
        unset($this->taskMap[$tid]);
        // This is a bit ugly and could be optimized so it does not have to walk the queue,
        // but assuming that killing tasks is rather rare I won't bother with it now
        foreach ($this->taskQueue as $i => $task) {
            if ($task->getTaskId() === $tid) {
                unset($this->taskQueue[$i]);
                break;
            }
        }
        return true;
    }
}

class SystemCall
{
    protected $callback;
    public function __construct(callable $callback)
    {
        $this->callback = $callback;
    }
    public function __invoke(Task $task, Scheduler $scheduler)
    {
        $callback = $this->callback;
        return $callback($task, $scheduler);
    }
}

function getTaskId()
{
    return new SystemCall(function (Task $task, Scheduler $scheduler) {
        $task->setSendValue($task->getTaskId());
        $scheduler->schedule($task);
    });
}

function newTask(Generator $coroutine)
{
    return new SystemCall(
        function (Task $task, Scheduler $scheduler) use ($coroutine) {
            $task->setSendValue($scheduler->newTask($coroutine));
            $scheduler->schedule($task);
        }
    );
}

function killTask($tid)
{
    return new SystemCall(
        function (Task $task, Scheduler $scheduler) use ($tid) {
            $task->setSendValue($scheduler->killTask($tid));
            $scheduler->schedule($task);
        }
    );
}

function childTask()
{
    $tid = (yield getTaskId());
    while (true) {
        echo "Child task $tid still alive!\n";
        yield;
    }
}

function task()
{
    // 调度器第一次 $task->run 执行current 调用getTaskId() sendValue = 1

    // 调度器第二次 $task->run send(1) $tid = 1 并且执行了newTask(childTask())
    // newTask(childTask) 先入队
    // 执行newTask中的闭包 传入的是task是当前task而不是childTask 并再次入队 此时sendValue = 2

    // 调度器第三次 $task->run 执行了childTask -> current 设置了sendValue = 2 并再次入队 

    // 调度器第四次 $task->run 切换到了主(父)task send(2) 此时childTid = 2
    // 并且输出Parent task 1 iteration 1. 失去执行权后在调度器中重新入队
    
    // 调度器第五次 $task->run 切换到了childTask send(2) $tid = 2
    // 并且输出Child task 2 still alive! 失去执行权后在调度器中重新入队

    // 调度器第六次 $task->run 主task send(null) 
    // 并且输出Parent task 1 iteration 2. 失去执行权后在调度器中重新入队

    // 调度器第七次 $task->run childTask send(null)
    // 并且输出Child task 2 still alive!  失去执行权后在调度器中重新入队

    // 调度器第八次 $task->run 主task send(null) 
    // 并且输出Parent task 1 iteration 3. 失去执行权后在调度器中重新入队
    // 并且执行killTask($childTid) send返回SystemCall实例 调度器自动调用返回实例killTask($childTid)
    // 调用调度器的killTask方法 在失去执行权之前直接入队

    // 调度器第九次 $task->run childTask send(null) 
    // 并且输出Child task 2 still alive! 失去执行权后在调度器中重新入队

    // 调度器第十次 $task->run 主task send(null)
    // 此时i==3 send返回的是SystemCall实例 调度器执行killTask($childTid) 在调度器队列中删除了childTask
    // 并且主携程重新入队

    // ...
    // 直到调度器判定任务全部finished
    $tid = (yield getTaskId());
    $childTid = (yield newTask(childTask()));
    for ($i = 1; $i <= 6; ++$i) {
        echo "Parent task $tid iteration $i.\n";
        yield;
        if ($i == 3) yield killTask($childTid);
    }
}

$scheduler = new Scheduler;
$scheduler->newTask(task());
$scheduler->run();

真的很烧脑啊,再次膜拜nikic大神。

鸟哥博客

nikic原文

阮一峰spl学习笔记

发现错误,欢迎指导,感谢!!!

posted @ 2020-12-10 22:42  alwayslinger  阅读(251)  评论(0编辑  收藏  举报