zoukankan      html  css  js  c++  java
  • 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}
    ";
                $val = (yield $j => $each);
                yield; // ignore foreach's next()
                echo "getGenerator(): received: {$j} => {$val}
    ";
            }
        }
    }
    $x = new X;
    
    foreach ($x as $i => $val){
        echo "getIterator(): {$i} => {$val}
    ";
    }
    echo "
    ";
    
    $gen = $x->getGenerator();
    foreach ($gen as $j => $val){
        echo "getGenerator(): sending:  {$j} => {$val}
    ";
       	// $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}
    ";
        // $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!
    ";
            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.
    ";
            yield;
            if ($i == 3) yield killTask($childTid);
        }
    }
    
    $scheduler = new Scheduler;
    $scheduler->newTask(task());
    $scheduler->run();
    

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

    鸟哥博客

    nikic原文

    阮一峰spl学习笔记

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

  • 相关阅读:
    小作业5
    小作业4
    Matplotlib基础
    Numpy学习笔记(下篇)
    Numpy学习笔记(上篇)
    机器学习的经典算法与应用
    opencv学习笔记D01
    Markdown新手入门
    python之生成器yeild
    pyinstaller打包.py程序为.exe操作指南
  • 原文地址:https://www.cnblogs.com/alwayslinger/p/14117924.html
Copyright © 2011-2022 走看看