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可以看到程序并不是交替执行的,不是协作的方式。
改一下:
待续……