【swoole.2.05】使用协程前需要了解的:Generator,Iterator,yield

之前应该有说过,协程是语言层面的多进程,其主要思想是遇到io阻塞的时候保存当前栈信息作为上下文,让出代码执行权限,等io有返回之后再回去执行协程未执行完的代码。看起来比较难理解,所以在了解协程之前,先带大家了解下yield,Generator和Iterator

名词解释

Iterator

Iterator是一个迭代器接口,写php的朋友们可能对这个词比较陌生,写java的朋友们一眼就认出来了,这就是java中用来遍历数组的最简单的包:java.util.Iterator。当然其他语言也是一样的,他们都叫迭代器,作用也都是用来循环遍历的。一个标准的Iterator需要实现5个接口:

  1. current():获取当前指针的元素
  2. key():获取当前指针
  3. next():移动到下一个指针
  4. rewind():将指针移动到开头
  5. valid():检查当前位置是否有效

如果你有使用指针操作过数组的话这些函数应该并不陌生,没有用过也没关系,就是这5个接口组成了大家常用的foreach。写一个类实现Iterator试一试他们的执行顺序。

代码
class Iterators implements Iterator
{
    private $key = null;
    private $valus = [
        'a', 'b', 'c', 'd'
    ];

    public function __construct()
    {
        $this->key = 0;
    }

    public function current()
    {
        // TODO: Implement current() method.
        var_dump(__METHOD__);
        return $this->valus[$this->key];
    }

    public function next()
    {
        // TODO: Implement next() method.
        var_dump(__METHOD__);
        $this->key++;
    }

    public function key()
    {
        // TODO: Implement key() method.
        var_dump(__METHOD__);
        return $this->key;
    }

    public function valid()
    {
        // TODO: Implement valid() method.
        var_dump(__METHOD__);
        return isset($this->valus[$this->key]);
    }

    public function rewind()
    {
        // TODO: Implement rewind() method.
        var_dump(__METHOD__);
        $this->key = 0;
    }

}

$test = new Iterators();

foreach ($test as $k => $v) {
    var_dump("key:" . $k);
    var_dump('value:' . $v);
}

结果

λ php test.php
string(17) "Iterators::rewind"
string(16) "Iterators::valid"
string(18) "Iterators::current"
string(14) "Iterators::key"
string(5) "key:0"
string(7) "value:a"
string(15) "Iterators::next"
string(16) "Iterators::valid"
string(18) "Iterators::current"
string(14) "Iterators::key"
string(5) "key:1"
string(7) "value:b"
string(15) "Iterators::next"
string(16) "Iterators::valid"
string(18) "Iterators::current"
string(14) "Iterators::key"
string(5) "key:2"
string(7) "value:c"
string(15) "Iterators::next"
string(16) "Iterators::valid"
string(18) "Iterators::current"
string(14) "Iterators::key"
string(5) "key:3"
string(7) "value:d"
string(15) "Iterators::next"
string(16) "Iterators::valid"

通过上面的小案例可以看到,一个foreach的执行是这样的:

  1. 首先会调用rewind()方法将数组对象的指针移动到开头
  2. 使用valid()方法判断当前指针是否有元素
  3. 如果没有元素,这里会直接中断
  4. 如果有元素,继续执行 1. 使用current()获取当前元素 1. 使用key()获取当前指针 1. 使用next()将指针移动到下一个位置并重复2.的动作

这就是一个标准的迭代器啦!

Generator

Generator是一个生成器类,在实现了Iterator迭代器的基础上还增加了3个方法(也可以说增加了两个)

  1. send():向生成器中传入一个值
  2. throw():向生成器中抛出一个异常
  3. __wakeup():序列化回调

关于Generator,需要了解的不是太多,只需要记得它重构了__wakeup序列化回调方法就行了。

yield

yield是一个生成器,当一个生成器被调用的时候,它返回一个可以被遍历的对象。当你遍历这个对象的时候(例如通过一个foreach循环),PHP 将会在每次需要值的时候调用生成器函数,并在产生一个值之后保存生成器的状态,这样它就可以在需要产生下一个值的时候恢复调用状态。

一旦不再需要产生更多的值,生成器函数可以简单退出,而调用生成器的代码还可以继续执行,就像一个数组已经被遍历完了。

在php中,yield关键字只能在函数中使用,在函数外使用会抛出致命错误

yield" expression can only be used inside a function

而且使用了yield关键字的函数都会返回一个Generator对象。

yield语句有点像return语句,代码执行到yield语句,函数的执行就会终止,并且会返回yield语句中的表达式的值给Generator对象,这跟return语句一样,不同的是,这返回值只是作为遍历Generator对象的当前元素,而不能赋值给其他变量。当对Generator对象继续迭代,函数中的yield后面的代码会继续执行,直到函数中的yield语句全部执行完毕。

function test()
{
    echo "test function start" . PHP_EOL;
    yield 'a';
    echo "inside test function echo a" . PHP_EOL;
    yield 'b';
    echo "inside test function echo b" . PHP_EOL;
    yield 'c';
    echo "inside test function echo c" . PHP_EOL;
}

$gen = test();
var_dump($gen);
var_dump($gen->rewind());
while ($gen->valid()) {
    var_dump($gen->valid());
    var_dump($gen->current());
    echo "continue" . PHP_EOL;
    var_dump($gen->next());
}
λ php test.php
object(Generator)#1 (0) {
}
test function start
NULL
bool(true)
string(1) "a"
continue
inside test function echo a
NULL
bool(true)
string(1) "b"
continue
inside test function echo b
NULL
bool(true)
string(1) "c"
continue
inside test function echo c
NULL

从执行结果上可以看到,调用了上面的test()函数,函数里有一堆echo,包括函数的第一行,但是在调用时没有任何输出,var_dump($gen)显示这个函数返回了一个Generator生成器对象。

在下面的代码中,仿照foreach,首先使用rewind()将指针移动到开头,这个时候函数内的代码才开始执行,输出了test function start,在执行到yield 'a';的时候,函数停止运行。这个时候生成器有值了。

之后再一个while循环中,使用valid()方法判断当前指针有元素后使用current()方法获取了当前生成器的值,也就是yield关键字返回给生成器的数据。这个时候的代码并不会继续执行,直到使用next()将指针指向下一个元素的时候,生成器才会继续执行方法里的代码,输出了inside test function echo a,然后反复循环。

通过上面的例子可以看出来,又yield关键字的方法并不会立刻执行,而是直接返回一个Generator生成器,而当移动Generator生成器的指针的时候,方法内的代码才会执行。那么问题来了,它有什么用?它和协程有什么关系?

Generator和协程的关系

首先,搬出我最尊敬的鸟哥的文章:在PHP中使用协程实现多任务调度

协程的支持是在迭代生成器的基础上, 增加了可以回送数据给生成器的功能(调用者发送数据给被调用的生成器函数). 这就把生成器到调用者的单向通信转变为两者之间的双向通信.

如鸟哥所说,协程的支持是在迭代生成器的基础上,也就是在本文开头所说的让住代码执行权限

其主要思想是遇到io阻塞的时候保存当前栈信息作为上下文,让出代码执行权限

向Generator内传递数据

向Generator内传递数据是使用的send()方法,首先展示一下使用send()向迭代器中传递数据

function gen() {
    while (true){
        echo yield . PHP_EOL;
    }
}

$gen = gen();
$gen->send('a');
$gen->send('b');
λ php test.php
a
b

之前,使用yield让生成器里的代码通过yield关键字向外传递了数据。这里使用send()方法的时候,yield()方法却是在向方法中传递数据。这就是yield的神奇之处,它既可以向方法外传递数据,也可以向方法内传递数据,那么开始一个很迷的操作。

function gen() {
    while (true){
        echo yield rand(0,10). PHP_EOL;
    }
}

$gen = gen();
var_dump($gen->send('a'));
var_dump($gen->send('b'));
D:\_dev\swoole\2.05
λ php test.php
a
int(5)
b
int(5)

可以看到,调用send()方法向函数内部发送了一个a,函数内部开始执行,输出了a,这个时候执行到了yield rand(0,10),yield关键字又向外传递了rand(0,10)的内容,也就是整型5,可能我了解到的面不够,在接触到yield之前,没有见过任何一个php关键字或者函数可以做到这种神奇的事情。但是正是这种可以向内和向外传递数据的特性才实现了协程。

yield的使用场景

如鸟哥的文章里所说,yield可以解决大内存处理的问题,如想要使用range()生成一个巨大的数组并遍历,直接使用php会占用巨大的内存保存这个变量,使用yield生成一个生成器慢慢遍历可以利用代码延迟执行的特性,减少很多内存。这里给一个demo。

function test($start, $end)
{
    for ($i = $start; $i < $end; $i++) {
        yield $i;
    }
}

$m = memory_get_usage();
$test = test(0, 100000);
foreach ($test as $k => $v) {

}
echo '内存使用:' . (memory_get_usage() - $m) . PHP_EOL;

$m = memory_get_usage();
$test = range(0, 100000);
foreach ($test as $k => $v) {

}
echo '内存使用:' . (memory_get_usage() - $m);
λ php test.php
内存使用:384
内存使用:6291152

关于Generator,Iterator,yield的相关知识就介绍到这里了。下一篇正是开始使用swoole的协程

程序幼儿员-龚学鹏
请先登录后发表评论
  • latest comments
  • 总共0条评论