迭代

概念

每一次对过程的重复称为一次“迭代”,而每一次迭代得到的结果会作为下一次迭代的初始值。
重复执行程序中的循环,直到满足某条件为止,称为迭代。

算法

迭代算法是用计算机解决问题的一种基本方法。它利用计算机运算速度快、适合做重复性操作的特点,让计算机对一组指令(或一定步骤)进行重复执行,在每次执行这组指令(或这些步骤)时,都从变量的原值推出它的一个新值。

RUP(统一软件开发过程)周期模型

瀑布模型

先定义需求,然后构建框架,然后写代码,然后测试,最后发布一个产品。大家才能见到一个产品。
我们开发一个产品,如果不太复杂,会采用瀑布模型
缺点:
如果对用户的需求判断的不是很准确,最后发布的是不可用的产品,项目失败

RUP周期模型(迭代方法)

假如这个产品要求6个月交货,第一个月拿出一个产品来,当然,这个产品会很不完善,给客户看,让他们提意见。
之后,再花一个月,进一步改进,又拿出一个更完善的产品来,给客户看,让他们提意见。
就这样,产品能够逐渐逼近客户的要求
缺点:周期长、成本很高。
优势:在应付大项目、高风险项目,迭代的成本比项目失败的风险成本低得多

PHP生成器(英文:Generator)

https://www.php.net/manual/zh/language.generators.syntax.php
https://www.php.net/manual/zh/class.generator.php
https://blog.51cto.com/chinalx1/2089327

参考文章,验证生成器和return占用内存:https://www.jianshu.com/p/103cbf359971
在PHP中使用协程实现多任务调度:http://www.laruence.com/2015/05/28/3038.html

生成器简介

生成器是一种可中断的函数, 在它里面的yield构成了中断点
生成器允许你在 foreach 代码块中写代码来迭代一组数据而不需要在内存中创建一个数组, 那会使你的内存达到上限,或者会占据可观的处理时间。

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

生成器函数的核心是yield关键字。它最简单的调用形式看起来像一个return申明,不同之处在于普通return会返回值并终止函数的执行,而yield会返回一个值给循环调用此生成器的代码并且只是暂停执行生成器函数。

好处:

在处理大数据集合的时候不用一次性的加载到内存中。甚至你可以处理无限大的数据流

案例:

以往都是在后端生成大数据,在前端显示,就需要将这个大数据传入模版变量。
使用生成器的话,可以不生成这个大数据,而是yield产出每一个小数据,将负责yield的函数带入前端模版中,前端使用foreach遍历一次这个函数,产出一个值,如此反复。
如大文件,如后台查询100万的数据要在前台显示等,都可以不直接return这些大数据,而是通过yield的方式产出值。其实相当于在后端进行foreach这些大数据,每循环一次就echo出来。不同的是生成器可以直接在模版中或者其他脚本中使用

Iterator接口

生成器(Generator)提供了一种更容易的方法来实现简单的对象迭代(foreach遍历对象),相比较定义类实现 Iterator (迭代器)接口的方式,性能开销和复杂性大大降低。
我们通常使用foreach对数组进行遍历,如果要对对象进行遍历,那么这个对象的类就必须实现Iterator接口,并且实现Iterator接口所提供的5个方法:

  1. <?php
  2. Iterator extends Traversable {
  3. /* Methods */
  4. abstract public mixed current ( void ) //返回当前位置的元素
  5. abstract public scalar key ( void ) //返回当前元素对应的key
  6. abstract public void next ( void ) //移到指向下一个元素的位置
  7. abstract public void rewind ( void ) //倒回到指向第一个元素的位置
  8. abstract public boolean valid ( void ) //判断当前位置是否有效
  9. }

在foreach遍历过程中,这些方法都会被隐式调用,其中next()方法就是控制元素移动的,current()可以获取当前位置的元素。
Iterator接口扩展了Traversable接口,Traversable是一个空接口,它相当于一个标志,所有实现Iterator接口的类肯定也实现了Traversable接口,所以我们通常可以用下面的代码来判断一个变量是否可以通过foreach进行遍历:

<?php
if( !is_array( $items ) && !$items instanceof Traversable )
        //Throw exception here

Generator对象

也就是生成器,Generator是一个实现了Iterator接口的类。
Generator 对象不能通过 new 实例化,生成器关键字yield会返回一个Generator类的对象

<?php
Generator implements Iterator {
    /* 方法 */
    public current ( void ) : mixed  //返回当前产生的值
    public key ( void ) : mixed  //返回当前产生的键
    public next ( void ) : void  //生成器继续执行
    public rewind ( void ) : void //重置迭代器
    public send ( mixed $value ) : mixed  //向生成器中传入一个值
    public throw ( Exception $exception ) : void  // 向生成器中抛入一个异常
    public valid ( void ) : bool  //检查迭代器是否被关闭
   public __wakeup ( void ) : void  //序列化回调
}


它实现了Iterator中的5个方法,还提供了三个新方法,其中
__wakeup是一个魔术方法,用于序列化,Generator实现这个方法是为了防止序列化。另外两个新方法是throw和send

yield

在php中,yield关键字只能在函数中使用。而且使用了yield关键字的函数都会返回一个Generator对象,我们把这种函数叫做generator函数(我自己取的名字,用于区分普通函数)。

yield语句有点像return语句,代码执行到yield语句,generator函数的执行就会终止,并且会返回yield语句中的表达式的值给Generator对象,这跟return语句一样,不同的是,这返回值只是作为遍历Generator对象的当前元素,而不能赋值给其他变量。
当对Generator对象继续迭代,generator函数中的yield后面的代码会继续执行,直到generator函数中的yield语句全部执行完毕,或者是碰到generator函数中的空return语句(返回null的return语句),在generator函数中使用带有非null返回值的return语句会报编译错误。

<?php
function gen() {
    yield 1;
}
$g = gen();
echo $g->valid();    //1
echo $g->current();  //1
echo $g->next();
echo $g->valid();    //
echo $g->current();  //

上面的代码首先调用gen函数生成一个Generator对象赋值给变量$g,因为Generator对象实现了Iterator接口,所以可以直接使用Iterator接口中的方法。
首先调用valid方法,它会返回1,表示这个对象当前处于可迭代状态;
然后调用current方法,它也会输出1,就是yield所返回的值,它是当前迭代的元素的值,在这个示例中也是第一个元素;
紧接着会调用next方法,它会对Generator对象做一次迭代,就是把当前迭代的位置向下移动一位,然后再次调用valid(),这个时候输出为空,这表示对Generator的迭代已终止,
再次调用current()返回也是空值。

多个yield语句

<?php
function gen() {
    yield 1;
    yield 2;
    yield 3;
}

foreach (gen() as $key => $value) {
    echo '<pre>';
        print_r($value); //1 2 3
    echo '</pre>';
}

$g = gen();
echo $g->valid(); //1
echo $g->current();//1
echo "<br>";
echo $g->next(); //空
echo $g->valid();//1
echo $g->current(); //2
echo "<br>";
echo $g->next();//空
echo $g->valid();//1
echo $g->current();//3
echo "<br>";
echo $g->next();//空
echo $g->valid();//空
echo $g->current();//空

Generator对象的中可迭代的元素就是所有yield语句返回的值的集合,在这个示例中是[1,2,3]。看起来跟数组很像,但它跟数组有本质的区别,遍历Generator对象的每次迭代都只会执行前一次yield语句之后的代码,而且碰到yield语句就会返回一个值,相当于从generator函数中返回,这有点像挂起一个进程(线程)的执行(yield在很多语言中就是用于挂起进程(线程)),然后又启动它继续执行,周而复始直到进程(线程)执行中止,这也是为什么Generator可以用于实现协程的原因

当然我们一般不会写上面的代码,而是在generator函数中使用for循环,而遍历则使用foreach。当我们使用foreach遍历数组的时候,有时候会使用到数组的key,所以yield是否也可以返回键值对的形式呢?当然可以,而且yield还可以返回null,或者是返回引用,关于这些特殊的用法可以参考yield的文档

send方法

yield也可以用于表达式的上下文中,例如用于赋值语句的右侧:
$data = (yield $value);
注意这里必须使用圆括号,要不然则会产生解析错误。
这里的yield相当于一个表达式,它需要跟Generator对象中的send函数配合使用。send函数接收一个参数,它会将这个参数的值传递给Generator对象并作为当前yield表达式的结果,同时还会恢复generator函数的执行(调用一次next函数)。我们通过一个示例来说明这个过程:

<?php
//yield同时进行接收和发送
function gen() {
    $ret = (yield 'yield1');
    var_dump($ret);// 2 string(4) "ret1" (第一个send方法传递的值ret1)
    $ret = (yield 'yield2');
    var_dump($ret);// 4 string(4) "ret2" (第二个send方法传递的值ret2)
}

$gen = gen();
var_dump( $gen->current());// 1 string(6) "yield1",返回第一个yield默认值:yield1

//迭代器有send()方法,用于传递数据
var_dump($gen->send('ret1')); // 3 string(6) "yield2" (传递完ret1值之后返回的值,返回的是下一个yield的默认值yield2)

var_dump($gen->send('ret2')); // 5 NULL  (传递完ret2值之后返回的值,返回的是下一个yield的值,此时没有下一个yield了,所以是null)

任何时候yield关键词都即是语句——可以为generator函数返回值,也是表达式——可以接收Generator对象发过来的值。

协程:Generator对象和generator函数的通信

1 generator函数中的yield语句可以中止generator函数的执行,并且将代码执行权交给Generator对象。
这跟函数调用是类似的,return语句可以从子函数中退出,并返回到主函数的调用处,相当于将代码执行权转交给主函数。而且在多任务调度中也有这种模型,这叫非抢占式调度,就是由子任务主动交出调度权,而不是由某个任务调度器来管理,一些老的操作系统是按照这种模式来管理任务调度,现在采用的都是抢占式调度,会有一个调度管理器来根据任务的优先级以及运行情况来决定调度权,它也可以强制收回某个任务的调度权,这种模式叫做抢占式调度。

2 generator函数中的yield语句除了让渡调度权,还可以给Generator对象返回数据。
3 Generator对象可以控制generator函数的执行,可以接收generator函数返回的数据,它有两种控制generator函数执行的方式,一种是next方法,另外一种就是send方法。
4 Generator对象最重要的特点是可以给generator函数传递数据,这个也是send方法所做的事情。

实例:生成器函数中进行循环。

<?php
function nums() {
    for ($i = 0; $i < 5; ++$i) {
                //get a value from the caller
        $cmd = (yield $i);
        if($cmd == 'stop')
            return;//exit the function
        }     
}
$gen = nums();
foreach($gen as $v)
{
    if($v == 3)//we are satisfied
        $gen->send('stop');
    echo "{$v}\n";
}
//Output
0
1
2
3

在这个示例中对nums函数返回的Generator对象的遍历就是从nums函数中获取数据,这相当于从generator函数传递数据给Generator对象,而当Generator对象可以’stop’传递给nums函数来要求终止Generator的遍历了,这相当于从Generator对象到generator函数的通信。

再看一个例子:

<?php
//创建一个消费者consume协程
function consumer() {
    while (true) {
        $num = 0;
        $i = 0;
        $num = (yield $i);
        echo '-开始消费'.$num.'<br/>';

        //可以选择中断协程
            if($num == '3')
                return;//终止协程,
    }
}

$consumer = consumer();

//生产者:生产一百个数字
$number = range(0,5);
foreach($number as $v)
{
    echo '开始生产'.$v.'<br/>';
    //发送数据给 消费者consume协程
    $consumer->send($v);
}
//Output
开始生产0
-开始消费0
开始生产1
-开始消费1
开始生产2
-开始消费2
开始生产3
-开始消费3
开始生产4
开始生产5

创建了一个叫做consumer的协程,并且在主线程中生产数据,协程中消费数据。
当协程执行到yield关键字时,会执行完当次循环然后暂停循环执行,并返回将代码执行权交给Generator对象。等到主线程调用send方法发送数据,协程才会接到数据继续执行。
yield让协程暂停,和线程的阻塞是有本质区别的。协程的暂停完全由程序控制,线程的阻塞状态是由操作系统内核来进行切换。
因此,协程的开销远远小于线程的开销。

Generator的作用

如果不考虑用Generator来实现协程,那么Generator的一个最大的作用就是为含有大量数据的集合(当前这些数据集是规则的,就像xrange所返回的那些数据)的遍历节省空间,这一点是显而易见的,我们写一个简单的benchmark来测试一下:比较range函数跟xrange函数的时间和空间的开销,代码如下:

<?php
$n = 100000;
$startTime = microtime(true);
$startMemory = memory_get_usage();
$array = range(1, $n);
foreach($array as $a) {
}
echo memory_get_usage() - $startMemory, " bytes\n";
echo microtime(true) - $startTime. " ms\n";

function xrange($start,$end,$step=1) {
    for($i=$start;$i<$end;$i+=$step) {
        yield $i;
    }
}
$startTime = microtime(true);
$startMemory = memory_get_usage();
$g = xrange(1,$n);
foreach($g as $i) {
}
echo memory_get_usage() - $startMemory, " bytes\n";
echo microtime(true) - $startTime. " ms\n";

这段代码的输出为:

14649152 bytes
0.017426013946533 ms
408 bytes
0.012494802474976 ms

我的php版本是5.5.14,从这个测试可以看出,使用range函数生成一个含有100000个整数的数组,然后对这个数据进行遍历,它需要的存储空间为14M。而使用Generator则不需要生成出包含所有元素的数组,所以它的空间开销为408个字节,这个差别是非常惊人的,而且在运行时间上也更优,不过这一点上两种方式相差不多。

PHP生成器实例讲解

“yield” & “return” 的区别

function getValues() {
    return 'value';
}
var_dump(getValues()); // string(5) "value"

function getValues() {
    yield 'value';
}
var_dump(getValues()); // object(Generator)[1]

生成器 类实现了 生成器 接口, 是一个对象。必须通过遍历对象的方法来取值

foreach (getValues() as $value) {
   echo $value;//value
}

优点:

生成器会对PHP应用的性能有非常大的影响
PHP代码运行时节省大量的内存
比较适合计算大量的数据

概念引入

<?php
function createRange($number){
    $data = [];
    //循环多次,将每次的值都放入data数组中
    for($i=0;$i<$number;$i++){
        $data[] = time();
    }
    //当for循环执行完毕,返回data数组
    return $data;
}

$result = createRange(10); // 调用上面创建的函数
foreach($result as $value){
    sleep(1);//这里停顿1秒,我们后续有用(sleep不会停顿一秒echo一个值,然后在停顿一秒。脚本执行的时候,是会将所有的程序都执行完之后才会输出,所以这里会停顿10秒,然后将最终的值都打印出来。就像把sleep函数放到程序最后面,并不会先将前面的值都echo出来然后程序在sleep,而是会等sleep执行完成之后,才会一起输出)
    echo $value.'<br />';
    echo '1s间隔的时间:'.time().'<br />';
}

浏览器执行的时间大于十秒之后会将值打印出来,
第一个echo打印出来的值是十个一样的时间戳,createRange函数内的for循环结果被很快放到$data中,并且立即返回。所以,foreach循环的是一个固定的数组。
另一个echo是间隔一秒的时间,sleep1秒输出一个时间戳(最少间隔一秒,程序运行也需要时间)
1573608212
1s间隔的时间:1573608213
1573608212
1s间隔的时间:1573608214
1573608212
1s间隔的时间:1573608215
1573608212
1s间隔的时间:1573608216
1573608212
1s间隔的时间:1573608217
1573608212
1s间隔的时间:1573608219
1573608212
1s间隔的时间:1573608220
1573608212
1s间隔的时间:1573608221
1573608212
1s间隔的时间:1573608222
1573608212
1s间隔的时间:1573608223

思考一个问题

我们注意到,在调用函数createRange的时候给$number的传值是10,一个很小的数字。假设,现在传递一个值10000000(1000万)。
那么,在函数createRange里面,for循环就需要执行1000万次。且有1000万个值被放到$data里面,(时间戳有十位,一个数字占用一个字节,一个时间戳是10字节Byte, 1024Byte(字节)=1KB ,1024KB=1MB,所以1000万个值大约需要的空间是 1000万*10 /1024/1024 = 95MB),而$data数组是被放在内存内。所以,在调用函数时候会占用大量内存。
这里,生成器就可以大显身手了。

创建生成器

function createRange($number){
    for($i=0;$i<$number;$i++){
        yield time();//生成器关键字yield
    }
}

我们删除了数组$data,而且也没有返回任何内容,而是在time()之前使用了一个关键字yield

使用生成器

$result = createRange(10); // 这里调用上面我们创建的函数
foreach($result as $value){
    sleep(1);
    echo $value.'<br />';
}

程序在运行了十秒钟后输出了如下值:
1534745088 1534745089 1534745090 1534745091 1534745093 1534745094 1534745095 1534745096 1534745097 1534745098
这里的间隔一秒其实就是sleep(1)造成的后果

使用生成器时:createRange的值不是一次性快速生成,而是依赖于foreach循环。foreach循环一次,yield执行一次。sleep了一秒,就会等待一秒才会去foreach。

还原生成器代码执行过程

1 首先调用createRange函数,传入参数10,但是for值执行了一次然后停止了,并且告诉foreach第一次循环可以用的值。
2 foreach开始对$result循环,进来首先sleep(1),然后开始使用for给的一个值执行输出。
3 foreach准备第二次循环,开始第二次循环之前,它向for循环又请求了一次。
4 for循环于是又执行了一次,将生成的时间戳告诉foreach.
5 foreach拿到第二个值,并且输出。由于foreach中sleep(1),所以,for循环延迟了1秒生成当前时间
所以,整个代码执行中,始终只有一个记录值参与循环,内存中也只有一条信息。
无论开始传入的$number有多大,由于并不会立即生成所有结果集,所以内存始终是一条循环的值。而return是直接将一个大数组保存下载,然后才输出。yield是foreach循环一次echo一行的结果,然后yield的for循环执行一次。

生成器概念理解

生成器yield关键字不是返回值,他的专业术语叫产出值,只是生成一个值
那么代码中foreach循环的是什么?其实是PHP在使用生成器的时候,yield会返回一个Generator类的对象(美 [‘dʒɛnəretɚ])。foreach可以对该对象进行迭代,每一次迭代,PHP会通过Generator实例计算出下一次需要迭代的值。这样foreach就知道下一次需要迭代的值了。
而且,在运行中for循环执行后,会立即停止。等待foreach下次循环时候再次和for索要下次的值的时候,for循环才会再执行一次,然后立即再次停止。直到不满足条件不执行结束。

实际开发应用

1 读取超大文件

PHP开发很多时候都要读取大文件,比如csv文件、text文件,或者一些日志文件。这些文件如果很大,比如5个G。这时,直接一次性把所有的内容读取到内存中计算不太现实。

使用生成器读取文件,第一次读取了第一行,第二次读取了第二行,以此类推,每次被加载到内存中的文字只有一行,大大的减小了内存的使用。
这样,即使读取上G的文本也不用担心,完全可以像读取很小文件一样编写代码。

我们创建一个text文本文档,并在其中输入几行文字,示范读取。

<?php
header("content-type:text/html;charset=utf-8");
function readTxt()
{
    $handle = fopen("./test.txt", 'rb');
    //feof() 函数检测是否已到达文件末尾 (eof)。
    //输出文本中所有的行,直到文件结束为止。
    while (feof($handle)===false) {
                // fgets() 函数从文件指针中读取一行。
        yield fgets($handle);
    }
    fclose($handle);
}
foreach (readTxt() as $key => $value) {
    echo $value.'<br />';
}

问题:

面对大文件,也不需要向上面的方法这样处理,用fgets一行行地读,读了后马上就可以处理,不需要把读到的数据放到一个变量去,再一行行地循环出来处理。代码如下

<?php
$handle = fopen("./test.txt", 'rb');
while (feof($handle)===false) {
    //直接输出一行
    echo fgets($handle).'<br />';
}
fclose($handle);

这样确实是可以的,这种方式也是不需要将大数组放入内存中的,直接输出。但是这种方式有个问题就是在MVC模式中,前后端分离的操作中,如果我要把这些大文件输出到前端中,就不能直接在后端代码中echo出来了,需要在前端foreach出来。所以生成器就派上用场了。

2 大文件输出到前端界面里

laravel框架写法:

<?php
//控制器中:comtroller.php中。调用函数,返回视图
public function ceshi(){
    $result = $this->iMember->createRange(5);
    return view('Member.Management.Edit')->with('result',$result);
}

// iMember实现类中。实现createRange函数
public function createRange($number){
    for($i=0;$i<$number;$i++){
        yield time();//生成器关键字yield
    }
}

// Edit视图中。foreach一次yield一次。
<div class="modal-body">
    @foreach($result as $key=>$value)
        <?php sleep(1); ?>
        <p>{{ $value }}</p>
    @endforeach
</div>

界面输出如下:
PHP生成器 - 图1
这样就可以将大文件使用yield的方式输出到前段中,而不是return回一个大数组。

3 重新实现 range() 函数

range — 根据范围创建数组,包含指定的元素
array range ( mixed $start , mixed $end [, number $step = 1 ] )
参数
start
序列的第一个值。
end
序列结束于 end 的值。
step
如果设置了步长 step,会被作为单元之间的步进值。step 应该为正值。不设置step 则默认为 1。测试发现,不能设置为0会报错,设置为负数会按照正数执行
<?php
// range函数生成了一个1到12组成的数组
$array = range(0, 5);
//会创建一个数组
/*Array
(
    [0] => 0
    [1] => 1
    [2] => 2
    [3] => 3
    [4] => 4
    [5] => 5
)*/

foreach ($array as $number) {
    echo $number;//012345
}


改进方案:
我们可以实现一个 xrange() 生成器, 只需要创建 Iterator(迭代) 对象并在内部跟踪生成器的当前状态,这样只需要不到1K字节的内存。

<?php
function xrange($start, $limit, $step = 1) {
    if ($start < $limit) {
        if ($step <= 0) {
            throw new LogicException('Step must be 正数');
        }

        for ($i = $start; $i <= $limit; $i += $step) {
            yield $i;
        }
    } else {
        if ($step >= 0) {
            throw new LogicException('Step must be 负数');
        }

        for ($i = $start; $i >= $limit; $i += $step) {
            yield $i;
        }
    }
}

//range()和xrange()输出的结果是一样的。
foreach (range(1, 9, 2) as $number) {
    echo "$number ";//1 3 5 7 9
}
echo "\n";
foreach (xrange(1, 9, 2) as $number) {
    echo "$number ";//1 3 5 7 9
}

4 PDO查询+生成器

不从数据库一次获取全部的数据,而是使用yield一次产出一条记录。避免数据量大占用内存。类似读取大文件的例子,PDO也可以一次获取一行,并且指针移动一次
详情查找:laravel cursor 游标
laravel cursor 游标

5 协程