道者编程

PHP 协程

PHP的协程需要生成器(yield)配合,先看看迭代器。

一:迭代器(Iterator):在php中,数组可以遍历,对象也可以遍历,除此之外继承了Iterator接口的对象也可以遍历。

PHP内置的Iterator接口有5个方法:

Iterator::current — 返回当前元素
Iterator::key — 返回当前元素的键
Iterator::next — 向前移动到下一个元素
Iterator::rewind — 返回到迭代器的第一个元素
Iterator::valid — 检查当前位置是否有效

运行一下,查看运行顺序:

<?php
class MyIterator implements Iterator {
    private $position = 0;
    private $arr = array();
    
    public function __construct($list) {
        $this->arr = $list;
        $this->position = 0;
        echo "调用过程:";
    }
    // 返回到迭代器的第一个元素
    public function rewind() {
        echo __FUNCTION__.'->';
        $this->position = 0;
    }
    // 返回当前元素
    public function current() {
        echo __FUNCTION__.'->';
        return $this->arr[$this->position];
    }
    // 返回当前元素的键
    public function key() {
        echo __FUNCTION__;
        return $this->position;
    }
    // 向前移动到下一个元素
    public function next() {
        echo "调用过程:";
        echo __FUNCTION__.'->'; 
        ++$this->position;
    }
    // 检查当前位置是否有效,false结束遍历
    public function valid() {
        echo __FUNCTION__.'->'; 
        return isset($this->arr[$this->position]);
    }
    
}

$it = new MyIterator(range(1,5));
foreach($it as $key => $value) {
    echo ' ||| 键值对:key:'.$key.' value:'.$value;
}

输出:

调用过程:rewind->valid->current->key ||| 键值对:key:0 value:1
调用过程:next->valid->current->key ||| 键值对:key:1 value:2
调用过程:next->valid->current->key ||| 键值对:key:2 value:3
调用过程:next->valid->current->key ||| 键值对:key:3 value:4
调用过程:next->valid->current->key ||| 键值对:key:4 value:5
调用过程:next->valid->

从输出过程可以看到:首先返回第一个元素(rewind),然后检查当前位置是否有效(valid),如果有效,返回当前元素(current),返回当前元素键(key),然后迭代到下一个元素(next)……

这东西有什么好处?如果要读取的数组非常庞大,需要一次性读取的话,内存就会扛不住,通过迭代就能一点点的读出来。但是这种写法比较繁琐,PHP后来弄了一个东西,性能更好,更加方便,功能也更强大,那就是生成器:yield

二:生成器:yield

如果我们现在处理一个数组,100万,用常规方式,可以对比一下,当然不用yield,用上面的Iterator接口也可以,但实现起来麻烦,还必须写这5个方法,性能还不如yield。yield内部实现了对Iterator接口的支持。

<?php
function getCitys(){
    $citys = ['北京','上海','广州','深圳','铁岭'];
    foreach($citys as $key => $cityName) {
        yield $cityName;
    }
}
//这里返回的是一个Generator对象
$generator = getCitys();
//var_dump($generator);

foreach($generator as $cityName){
    echo $cityName.' ';
}

输出:北京 上海 广州 深圳 铁岭 

也可以输出键值对:

<?php
function getCitys(){
    $citys = ['北京','上海','广州','深圳','铁岭'];
    foreach($citys as $key => $cityName) {
        yield $key => $cityName; //键值对
    }
}
//这里返回的是一个Generator对象
$generator = getCitys();
//var_dump($generator);

foreach($generator as $key => $cityName){
    echo $key.':'.$cityName.' ';
}
输出:0:北京 1:上海 2:广州 3:深圳 4:铁岭

foreach这里也可以用Iterator接口:

<?php
function getCitys(){
    $citys = ['北京','上海','广州','深圳','铁岭'];
    foreach($citys as $key => $cityName) {
        yield $cityName;
    }
}
//这里返回的是一个Generator对象
$generator = getCitys();
//var_dump($generator);

foreach($generator as $cityName){
    echo $generator->key().':'; //输出key
    echo $generator->current().' '; //输出城市
}

输出:0:北京 1:上海 2:广州 3:深圳 4:铁岭 和上面是一样的。

也可以手动迭代:

<?php
function getCitys(){
    $citys = ['北京','上海','广州','深圳','铁岭'];
    foreach($citys as $key => $cityName) {
        yield $cityName;
    }
}
//这里返回的是一个Generator对象
$generator = getCitys();
//var_dump($generator);

echo $generator->current(); //输出第一个
$generator->next(); //往后面执行
echo $generator->current(); //输出第二个
这里迭代了一次,输出:北京上海

yield的好处就是,数据不一次性加载到内存,这样在处理大数据量的时候,比如导出大sql,excel,大文件等等,不至于把内存撑爆,可以轻松应对大文件。

搞个例子验证一下:

<?php
function times($max){
    for($i=0;$i<$max;$i++){
        $times[] = time();
    }
    return $times;
}

$result = times(10); // 这里已经取了10条数据

//遍历的时候把10条数据每隔1秒钟输出一次。
foreach($result as $value){
    sleep(1);//每次循环停1秒
    echo $value.'
';
}
 输出:

1568777667
1568777667
1568777667
1568777667
1568777667
1568777667
1568777667
1568777667
1568777667
1568777667
 可以看到10条数据是一样的,调用函数times的时候,就一次性把数据取出来了,只是遍历的时候1秒钟输出一次,for循环执行很快,别说执行10次,就是执行10万次也不要1秒钟,所以上面取值是一样的。

加yield关键字:

<?php
function times($max){
    for($i=0;$i<$max;$i++){
        yield time();
    }
}

$result = times(10); // 这里还没有执行

//这里才真正执行,每隔一秒钟提取一次
foreach($result as $value){
    sleep(1);//每次循环停1秒
    echo $value.'
';
}
输出:

1568777961
1568777962
1568777963
1568777964
1568777965
1568777966
1568777967
1568777968
1568777969
1568777970

发现问题没?这里输出都间隔了1秒钟,因为yield是迭代的,foreach 执行一次就迭代一次,内存中永远只有一组数据

yield关键字可以作为语句或者表达式:

作为语句会返回后面的值,变量,键值对等等,空的话返回null,作为表达式 可以接受send()函数传递的值。如:

Generator::send :向生成器中传入一个值。

https://www.php.net/manual/zh/generator.send.php

<?php
function getValue(){
   echo yield; //yield接受 send传来的值
}
$generator = getValue();
$generator->send('hello');

输出:hello

<?php
function nums() {
    for ($i = 0; $i <= 5; ++$i) {
        /*
        yield $i:返回$i
        $cmd = (yield $i) 是从外部传过来的,下面的foreach等于3的时候,传了 stop,那么这时候$cmd = 'stop'
        接着往下走,return 返回了,就不再迭代了。
        */
        $cmd = (yield $i);
        if($cmd == 'stop')
            return;//退出
        }     
}

$gen = nums();
foreach($gen as $v)
{
    if($v == 3)//发送 stop 停止迭代
        $gen->send('stop');
    echo "$v\n";
}

输出:0 1 2 3

yield 生成器是一个单向通信,send可以发送数据,这样实现了一个双向通信,这其实就是一个简单的协程。我们搞一个手动协程。

普通调用是这样的:

<?php
function task($caller)
{
    for ($i=0; $i<3; $i++ ) {
        echo '调度者:' . $caller . ' 输出:' . $i . '
';
    }
}
//两个调度者执行
task("李1");
task("李2");
输出:

调度者:李1 输出:0
调度者:李1 输出:1
调度者:李1 输出:2
调度者:李2 输出:0
调度者:李2 输出:1
调度者:李2 输出:2
可以看到程序并不是交替执行的,不是协作的方式。

改一下:

待续……


最新评论:
我要评论:

看不清楚