【未完成】这些函数你认识几个?快来鉴定你是初级|中级|还是高级p
php迭代器
- 我们先来举一个栗子,我这里有一个文件,大小是136m,
public function handle(){
$file=storage_path().'/data/demo.csv';
$size=filesize($file);
$size=round($size/(1024*1024),2);
echo "当前文件大小为{$size}m".PHP_EOL;
}
运行脚本
zhangguofu@zhangguofudeMacBook-Pro site (test-local) $ php artisan YieldTest
当前文件大小为136.18m
我们写一个方法 (设置最大运行内存128m)读取这个文件
/**
* Execute the console command.
*
* @return mixed
*/
public function handle(){
ini_set('memory_limit', '128M');//我们设置最大运行内存是128m,为了演示效果,小于filesize即可
$file=storage_path().'/data/demo.csv';
$res=file_get_contents($file);
dd($res);
}
//这个时候运行就会报错了
Whoops\Exception\ErrorException : Allowed memory size of 134217728 bytes exhausted (tried to allocate 142802448 bytes)
使用传统按行读取,也会报错,但是这个报错并不是fget报的错误,而是数组超出了内存限制
/**
* Execute the console command.
*
* @return mixed
*/
public function handle(){
ini_set('memory_limit', '128M');//我们设置最大运行内存是128m,为了演示效果,小于filesize即可
$file=storage_path().'/data/demo.csv';
$res=$this->readLocalFile($file);
dd($res);
}
/**
* Notes:加载本地文件,传统方式按行返回
* User: zhangguofu
* Date: 2021/1/25
* Time: 11:36 上午
* @param $fileName
* @return mixed
*/
public function readLocalFile($fileName)
{
$handle = fopen($fileName, 'r');
$lins = [];
while (!feof($handle)) {
$lines[] = fgets($handle);
}
fclose($handle);
return $lines;
}
image.png
- sleep 的栗子
<?php
function gen1(){
for ($i=1;$i<11;$i++){
sleep(1);
yield $arr[$i]=$i;
};
}
foreach (gen1() as $v){
echo $v;
}
- 那么这时我们就是要处理这个文件,怎么办呢?这个时候我们会不会思考一个问题呢,我用fgets获取数据后,如果不存数组,而是拿到一行数据就返回一行数据,行不行呢?这就是yield的思想
我们来看一下yield官方解释:
生成器函数看起来像普通函数——不同的是普通函数返回一个值,而生成器可以 yield 生成多个想要的值。 任何包含 yield 的函数都是一个生成器函数。
当一个生成器被调用的时候,它返回一个可以被遍历的对象.当你遍历这个对象的时候(例如通过一个foreach循环),PHP 将会在每次需要值的时候调用对象的遍历方法,并在产生一个值之后保存生成器的状态,这样它就可以在需要产生下一个值的时候恢复调用状态。
一旦不再需要产生更多的值,生成器可以简单退出,而调用生成器的代码还可以继续执行,就像一个数组已经被遍历完了。
- 接下来我们用代码来看一下yield怎么处理业务,这里为了演示,我每次sleep一秒
/**
* Execute the console command.
*
* @return mixed
*/
public function handle(){
ini_set('memory_limit', '128M');//我们设置最大运行内存是128m,为了演示效果,小于filesize即可
$file=storage_path().'/data/demo.csv';
$lines = $this->readYieldFile($file);//所有的数据都从生成器里面读取
foreach ($lines as $row) {
echo $row;
}
}
/**
* Notes:使用yield迭代返回文件
* User: zhangguofu
* Date: 2021/1/25
* Time: 11:37 上午
* @param $fileName
* @return Generator
*/
public function readYieldFile($fileName)
{
$handle = fopen($fileName, 'r');
$i=1;
while (!feof($handle)) {
sleep(1);
$line= fgets($handle);
$i++;
echo "第{$i}秒".PHP_EOL;
yield $line;
}
fclose($handle);
}
-
运行查看结果,发现我们的程序可以正常运行,数据可以读取并处理了
image.png
到此,总结一下,什么时候可能会用到yield
- 简单点说,只要是读取大文件的时候都可以用打。如果你工作中也遇到过Allowed memory size of 134217728 bytes exhausted 这类的错误,那么不妨试试yield
- 使用场景:读取日志文件,分析日志;excel,csv等大文件的数据导入,从数据库读取大量的数据,比如下面这个例子
<?php
function getFruit($conn) {
$sql = 'SELECT name, color, calories FROM fruit ORDER BY name';
foreach ($conn->query($sql) as $row) {
yield($row);
}
}
?>
这里我们再补充一下yield的常用操作
# http://php.net/manual/zh/class.generator.php
Generator implements Iterator {
/* Methods */
//获取迭代器当前值
public mixed current ( void )
//获取迭代器当前值
public mixed getReturn ( void )
//返回当前产生的键
public mixed key ( void )
//生成器从上一次yield处继续执行
public void next ( void )
//重置迭代器
public void rewind ( void )
//向生成器中传入一个值
public mixed send ( mixed $value )
//向生成器中抛入一个异常
public mixed throw ( Throwable $exception )
//检查迭代器是否被关闭
public bool valid ( void )
//迭代器序列化时执行的方法
public void __wakeup ( void )
}
不仅于此,yield 还可以生成带key的值
function gen($max)
{
for ($i=0; $i<$max; $i++) {
yield $i => $i+1;
}
return $max;
}
$gen = gen(5);
//var_dump($gen->key());
//var_dump($gen->current());
foreach ($gen as $key=>$val) {
var_dump($key . "=>" . $val);
}
# output
string(4) "0=>1"
string(4) "1=>2"
string(4) "2=>3"
string(4) "3=>4"
string(4) "4=>5"
- 这时候是不是感觉有点问题 ,就是 如果yield只是迭代返回值的话,那我们直接把代码写在 第一个循环里面就行了,为什么 还要使用迭代器,并且循环迭代器来获取值呢?这样想也是没有问题的。看很多文章,大多会说yield处理大文件更节省内存,但是真实是这样吗?yield 会节省内存吗?那我们通过栗子来看一下
<?php
function readTheFile($path) {
// $lines = [];
$handle = fopen($path, 'r');
while(!feof($handle)) {
$lines = trim(fgets($handle));
// yield $lines;
}
fclose($handle);
echo $lines;
}
readTheFile('luxun.txt');
function formatBytes($bytes, $precision = 2) {
$units = array('b', 'kb', 'mb', 'gb', 'tb');
$bytes = max($bytes, 0);
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1);
$bytes /= (1 << (10 * $pow));
return round($bytes, $precision) . ' ' . $units[$pow];
}
print formatBytes(memory_get_peak_usage());//
echo PHP_EOL;
输出结果:
zhangguofu@zhangguofudeMacBook-Pro default (master) $ php readFile1.php
424.71 kb
打开yield注释
<?php
function readTheFile($path) {
// $lines = [];
$handle = fopen($path, 'r');
while(!feof($handle)) {
$lines = trim(fgets($handle));
yield $lines;
}
fclose($handle);
echo $lines;
}
readTheFile('luxun.txt');
function formatBytes($bytes, $precision = 2) {
$units = array('b', 'kb', 'mb', 'gb', 'tb');
$bytes = max($bytes, 0);
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1);
$bytes /= (1 << (10 * $pow));
return round($bytes, $precision) . ' ' . $units[$pow];
}
echo "111111".PHP_EOL;
print formatBytes(memory_get_peak_usage());
echo PHP_EOL;
输出结果
zhangguofu@zhangguofudeMacBook-Pro default (master) $ php readFile1.php
111111
424.93 kb
通过上面这个栗子说明什么?
yield并不是节省了你多少内存,而是 传统方式在内存里面使用大数组来存储数据,导致php的内存很大,而 yield 像是一个指针,你可以像指针一样操作它,像current,next等方法。而它也会只生成一条数据在内存里面,下次的迭代是实现了内存中数据的替换。
yield的异步
因为yield 关键字的出现,就是为了让出cpu给下一个程序(协程)使用!我们看一下下面的代码,为了方便观察,我在每行输出都加了时间戳
<?php
echo "#############################";
$time=time();
function gen1($time){
for ($i = 1; $i <= 10; ++$i) {
echo PHP_EOL;
echo "第".(time()-$time)."秒"."----11111----This is task 1 iteration $i.\n";
yield;
echo "第".(time()-$time)."秒"."----1111";
echo PHP_EOL;
sleep(2);
echo "第".(time()-$time)."秒"."----1111---sleep完毕";
}
}
function gen2($time){
for ($i = 1; $i <= 5; ++$i) {
echo PHP_EOL;
echo "第".(time()-$time)."秒"."----22222----This is task 2 iteration $i.\n";
yield;
echo "第".(time()-$time)."秒"."----2222";
echo PHP_EOL;
sleep(1);
echo "第".(time()-$time)."秒"."----2222---sleep完毕";
}
}
//我先来生成两个迭代器
$gen1=gen1($time);
$gen2=gen2($time);
$i=1;
while ($i<11){
echo "这是第 {$i}次循环";
echo PHP_EOL;
var_dump( $gen1->current(),"this is 111111--var_dump--"."第".(time()-$time)."秒");
var_dump( $gen2->current(),"this is 222222--var_dump--"."第".(time()-$time)."秒");
$gen1->next();//回复中断
$gen2->next();//回复中断
$i++;
}
- 我们看一下输出结果
zhangguofu@zhangguofudeMacBook-Pro default (master) $ php send2.php
#############################这是第 1次循环
第0秒----11111----This is task 1 iteration 1.
/Users/zhangguofu/website/default/send2.php:52:
NULL
/Users/zhangguofu/website/default/send2.php:52:
string(33) "this is 111111--var_dump--第0秒"
第0秒----22222----This is task 2 iteration 1.
/Users/zhangguofu/website/default/send2.php:53:
NULL
/Users/zhangguofu/website/default/send2.php:53:
string(33) "this is 222222--var_dump--第0秒"
第0秒----1111
第2秒----1111---sleep完毕
第2秒----11111----This is task 1 iteration 2.
第2秒----2222
第3秒----2222---sleep完毕
第3秒----22222----This is task 2 iteration 2.
这是第 2次循环
/Users/zhangguofu/website/default/send2.php:52:
NULL
/Users/zhangguofu/website/default/send2.php:52:
string(33) "this is 111111--var_dump--第3秒"
/Users/zhangguofu/website/default/send2.php:53:
NULL
/Users/zhangguofu/website/default/send2.php:53:
string(33) "this is 222222--var_dump--第3秒"
第3秒----1111
第5秒----1111---sleep完毕
第5秒----11111----This is task 1 iteration 3.
第5秒----2222
第6秒----2222---sleep完毕
第6秒----22222----This is task 2 iteration 3.
这是第 3次循环
/Users/zhangguofu/website/default/send2.php:52:
NULL
/Users/zhangguofu/website/default/send2.php:52:
string(33) "this is 111111--var_dump--第6秒"
/Users/zhangguofu/website/default/send2.php:53:
NULL
/Users/zhangguofu/website/default/send2.php:53:
string(33) "this is 222222--var_dump--第6秒"
第6秒----1111
第8秒----1111---sleep完毕
第8秒----11111----This is task 1 iteration 4.
第8秒----2222
第9秒----2222---sleep完毕
第9秒----22222----This is task 2 iteration 4.
这是第 4次循环
/Users/zhangguofu/website/default/send2.php:52:
NULL
/Users/zhangguofu/website/default/send2.php:52:
string(33) "this is 111111--var_dump--第9秒"
/Users/zhangguofu/website/default/send2.php:53:
NULL
/Users/zhangguofu/website/default/send2.php:53:
string(33) "this is 222222--var_dump--第9秒"
第9秒----1111
第11秒----1111---sleep完毕
第11秒----11111----This is task 1 iteration 5.
第11秒----2222
第12秒----2222---sleep完毕
第12秒----22222----This is task 2 iteration 5.
这是第 5次循环
/Users/zhangguofu/website/default/send2.php:52:
NULL
/Users/zhangguofu/website/default/send2.php:52:
string(34) "this is 111111--var_dump--第12秒"
/Users/zhangguofu/website/default/send2.php:53:
NULL
/Users/zhangguofu/website/default/send2.php:53:
string(34) "this is 222222--var_dump--第12秒"
第12秒----1111
第14秒----1111---sleep完毕
第14秒----11111----This is task 1 iteration 6.
第14秒----2222
第15秒----2222---sleep完毕这是第 6次循环
/Users/zhangguofu/website/default/send2.php:52:
NULL
/Users/zhangguofu/website/default/send2.php:52:
string(34) "this is 111111--var_dump--第15秒"
/Users/zhangguofu/website/default/send2.php:53:
NULL
/Users/zhangguofu/website/default/send2.php:53:
string(34) "this is 222222--var_dump--第15秒"
第15秒----1111
第17秒----1111---sleep完毕
第17秒----11111----This is task 1 iteration 7.
这是第 7次循环
/Users/zhangguofu/website/default/send2.php:52:
NULL
/Users/zhangguofu/website/default/send2.php:52:
string(34) "this is 111111--var_dump--第17秒"
/Users/zhangguofu/website/default/send2.php:53:
NULL
/Users/zhangguofu/website/default/send2.php:53:
string(34) "this is 222222--var_dump--第17秒"
第17秒----1111
第19秒----1111---sleep完毕
第19秒----11111----This is task 1 iteration 8.
这是第 8次循环
/Users/zhangguofu/website/default/send2.php:52:
NULL
/Users/zhangguofu/website/default/send2.php:52:
string(34) "this is 111111--var_dump--第19秒"
/Users/zhangguofu/website/default/send2.php:53:
NULL
/Users/zhangguofu/website/default/send2.php:53:
string(34) "this is 222222--var_dump--第19秒"
第19秒----1111
第21秒----1111---sleep完毕
第21秒----11111----This is task 1 iteration 9.
这是第 9次循环
/Users/zhangguofu/website/default/send2.php:52:
NULL
/Users/zhangguofu/website/default/send2.php:52:
string(34) "this is 111111--var_dump--第21秒"
/Users/zhangguofu/website/default/send2.php:53:
NULL
/Users/zhangguofu/website/default/send2.php:53:
string(34) "this is 222222--var_dump--第21秒"
第21秒----1111
第23秒----1111---sleep完毕
第23秒----11111----This is task 1 iteration 10.
这是第 10次循环
/Users/zhangguofu/website/default/send2.php:52:
NULL
/Users/zhangguofu/website/default/send2.php:52:
string(34) "this is 111111--var_dump--第23秒"
/Users/zhangguofu/website/default/send2.php:53:
NULL
/Users/zhangguofu/website/default/send2.php:53:
string(34) "this is 222222--var_dump--第23秒"
第23秒----1111
第25秒----1111---sleep完毕这是第 11次循环
/Users/zhangguofu/website/default/send2.php:52:
NULL
/Users/zhangguofu/website/default/send2.php:52:
string(34) "this is 111111--var_dump--第25秒"
/Users/zhangguofu/website/default/send2.php:53:
NULL
/Users/zhangguofu/website/default/send2.php:53:
string(34) "this is 222222--var_dump--第25秒"
这是第 12次循环
/Users/zhangguofu/website/default/send2.php:52:
NULL
/Users/zhangguofu/website/default/send2.php:52:
string(34) "this is 111111--var_dump--第25秒"
/Users/zhangguofu/website/default/send2.php:53:
NULL
/Users/zhangguofu/website/default/send2.php:53:
string(34) "this is 222222--var_dump--第25秒"
这是第 13次循环
/Users/zhangguofu/website/default/send2.php:52:
NULL
/Users/zhangguofu/website/default/send2.php:52:
string(34) "this is 111111--var_dump--第25秒"
/Users/zhangguofu/website/default/send2.php:53:
NULL
/Users/zhangguofu/website/default/send2.php:53:
string(34) "this is 222222--var_dump--第25秒"
这是第 14次循环
/Users/zhangguofu/website/default/send2.php:52:
NULL
/Users/zhangguofu/website/default/send2.php:52:
string(34) "this is 111111--var_dump--第25秒"
/Users/zhangguofu/website/default/send2.php:53:
NULL
/Users/zhangguofu/website/default/send2.php:53:
string(34) "this is 222222--var_dump--第25秒"
我们来解释一下 ,为什么会这样执行
/**
* 我把一个函数分为两个部分, 即yield 前半部分和yield后半部分
* 在gen1 分为 p1(yield前半部分) 和 n1(yield后半部分)
* 在gen2分为 p2 和 n2
*
*两个var_dump 分别为 d1 和d2 ,那么我们看程序怎么执行的
*
* 先来看第1次循环
* p1 -> d1 -> p2 ->d2 ->n1 ->p1->n2->p2
*
* 再来看第2次循环
* d1->d2->n1->p1->n2->p2
*
* 第三次循环
* d1->d2->n1->p1->n2->p2
*
* 只到最后一次循环
*
* d1->d2->n1->p1->n2->p2
*
*
*/
- 理清了执行顺序后我们开始分析 为什么 要这么执行
在第一次执行的时候,p1 执行 遇到yield让出cpu,开始执行dump,然后执行gen2,同样遇到 yield 让出cpu,执行dump,执行完毕后又来了一个next,执行next会发生什么情况呢?之前终端的程序开始执行,于是开始 执行 sleep 操作,执行完sleep 进入下一个for 循环,只到 遇到yield 有开始终端,这就是 p1 d1 p2 d2 n1p1 n2p2的过程,注意,在第一次循环之后,两个程序已经中断执行了,还差yield 后半部分没有执行;
第二次循环的时候,var_dump 和 current 两个操作,按道理是先执行current ,然后dump,但是 这个时候 gen1是让出cpu的,所以先执行了dump,然后 gen里面的程序开始执行。以下类似
- yield指令提供了任务中断自身的一种方法, 然后把控制交回给任务调度器. 因此协程可以运行多个其他任务. 更进一步来说, yield还可以用来在任务和调度器之间进行通信.比如api阻塞
- 比如我有一个方法,既要去请求api,还要写日志,那么通常来讲,只能是同步进行了对不对!如果一个接口返回需要5s,那么我写日志 需要在5s 以后,就阻塞在了,但是使用yield之后呢,我们来看一下
简单写一个接口,接口是 睡眠5s
image.png
大家来看下面我写的这个栗子
<?php
// 创建一对cURL资源
$ch1 = curl_init();
// 设置URL和相应的选项
curl_setopt($ch1, CURLOPT_URL, "http://127.0.0.1/api1.php");
curl_setopt($ch1, CURLOPT_HEADER, 0);
// 创建批处理cURL句柄
$mh = curl_multi_init();
// 增加1个句柄
curl_multi_add_handle($mh,$ch1);
// gen1中就是调用三方API,基于multi-curl实现
function gen1( $mh, $ch1 ) {
do {
$mrc = curl_multi_exec( $mh, $active );
// 请求发出后,让出cpu
$rs = yield;
echo "收到外部发送数据{$rs}".PHP_EOL;
} while( $active > 0 );
$ret = curl_multi_getcontent( $ch1 );
echo time().PHP_EOL;
echo $ret.PHP_EOL;
return false;
}
function gen2() {
file_put_contents("001",time().PHP_EOL,FILE_APPEND);
sleep(1);//为了方便看,我sleep了1秒
}
//我先来生成一个迭代器
$gen1=gen1($mh,$ch1);
while (true){
echo $gen1->current();//阻塞
gen2();//写文件
$gen1->send(time());
}
看一下输出,在 请求api 阻塞的时候,我可以继续写我的日志,或者其他操作,然后使用 send 更新 来刷新重启 程序状态
image.png
yield的send 方法
向生成器中传入一个值,并且当做 yield 表达式的结果,然后继续执行生成器。
如果当这个方法被调用时,生成器不在 yield 表达式,那么在传入值之前,它会先运行到第一个 yield 表达式。As such it is not necessary to "prime" PHP generators with a Generator::next() call (like it is done in Python).
就是 说我不仅可以从里面获取值,我还可以往里面发送值,作为yield 表达式