php yield学习笔记(一)
说明yield关键字的说明网上有很多(文末会附上相关博客链接),这里我只说明我认为最基础的东西。那就是搞明白Iterator方法的调用顺序,以及Iterator方法在foreach中的对应关系。
# 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方法单步调试代码,以便更直观的感受任务的切换和调度
# 这里介绍鸟哥那片著名博客提供的思路
一个任务就是一个协程,需要使用不同的任务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大神。
发现错误,欢迎指导,感谢!!!