PHP生成器(generator)和协程的实现方法详解
本文实例讲述了PHP生成器(generator)和协程的实现方法。分享给大家供大家参考,具体如下:
先说一些废话
PHP5.5以来,新的诸多特性又一次令PHP焕发新的光彩,虽然在本文写的时候已是PHP7alpha2发布后的一段时间,但此时国内依旧是php5.3的天下。不过我认为新的特性迟早会因为旧的版本的逐渐消失而变得越发重要,尤其是PHP7的正式版出来后,因此本文的目的就是为了在这之前,帮助一些PHPer了解一些他们从没有了解的东西。所以打算将以本篇作为博客中PHP知识补全系列文章的开篇。
其实在写本文之前,我对生成器以及基于此特性延伸出来的php的协程实现并没有比较直观的了解,主要是我个人水平并不是很高,属于典型的刚入了门的PHPer。所以在看了前段时间鸟哥(laruence)博客中对协程的讲解(参考链接:《PHP中使用协同程序实现合作多任务》)后,在我个人对本篇的理解上,针对那些比较难以理解的概念(包括我个人在理解这一概念的时候的难点),以一个更为通俗的方式去讲明白。当然由于本人也是刚刚去学习这一概念,所以有些不得当的地方在所难免,希望大神看见了请不吝赐教。
一切从Iterator和Generator开始
为便于新入门开发者理解,本文一半篇幅是讲述迭代器接口(Iterator)和Generator类的,对此已经理解的话,可以直接跳过。
迭代和迭代器
在理解本文大多数概念前,有必要知道迭代和迭代器。事实上,迭代大家都知道是什么,可是我不知道(真的,在此之前对这个概念没有系统了解)。迭代是指反复执行一个过程,每执行一次叫做一次迭代。实际上我们经常做这种事情,比如:
'#FF0000', 'green'=>'#00FF00', 'blue'=>'#0000FF' ]; foreach($mappingas$key=>$value){ printf("key:%d-value:%s\n",$key,$value); }
我们可以看到通过foreach对数组遍历并迭代输出其内容。在这一环节中,我们需要关注的重点是数组。虽然我们迭代的过程是foreach语句中的代码块,但实际上数组$mapping在每一次迭代中发生了变化,意味着数组内部也存在着一次迭代。如果我们把数组看做一个对象,foreach实际上在每一次迭代过程都会调用该对象的一个方法,让数组在自己内部进行一次变动(迭代),随后通过另一个方法取出当前数组对象的键和值。这样一个可通过外部遍历其内部数据的对象就是一个迭代器对象,其遵循的统一的访问接口就是迭代器接口(Iterator)。
PHP提供了一个统一的迭代器接口。关于迭代器PHP官方文档有更为详细的描述,建议去了解。
interfaceIteratorextendsTraversable { /** *获取当前内部标量指向的元素的数据 */ publicmixedcurrent(void) /** *获取当前标量 */ publicscalarkey(void) /** *移动到下一个标量 */ publicvoidnext(void) /** *重置标量 */ publicvoidrewind(void) /** *检查当前标量是否有效 */ publicbooleanvalid(void) }
我们来给出一个实例,去实现一个简单的迭代器:
classXrangeimplementsIterator { protected$start; protected$limit; protected$step; protected$i; publicfunction__construct($start,$limit,$step=0) { $this->start=$start; $this->limit=$limit; $this->step=$step; } publicfunctionrewind() { $this->i=$this->start; } publicfunctionnext() { $this->i+=$this->step; } publicfunctioncurrent() { return$this->i; } publicfunctionkey() { return$this->i+1; } publicfunctionvalid() { return$this->i<=$this->limit; } }
通过foreach遍历来看看这个迭代器的效果:
foreach(newXrange(0,10,2)as$key=>$value){ printf("%d%d\n",$key,$value); }
输出:
10
32
54
76
98
1110
至此我们看到了一个迭代器的实现。一些人在了解这一特性会很激动的将其应用在实际项目中,但有些则疑惑这有什么卵用呢?迭代器只是将一个普通对象变成了一个可被遍历的对象,这在有些时候,如一个对象StudentsContact,这个对象是用于处理学生联系方式的,通过addStudent方法注册学生,通过getAllStudent获取全部注册的学生联系方式数组。我们以往遍历是通过StudentsContact::getAllStudent()获取一个数组然后遍历该数组,但是现在有了迭代器,只要这个类继承这个接口,就可以直接遍历该对象获取学生数组,并且可以在获取之前在类的内部就对输出的数据做好处理工作。
当然用处远不止这么点,但在这里就不过多纠结。有一个在此基础上更为强大的东西,生成器。
生成器,Generator
虽然迭代器仅需继承接口即可实现,但依旧很麻烦,我们毕竟需要定义一个类并实现该接口所有方法,这十分繁琐。在一些情景下我们需要更简洁的办法。生成器提供了一种更容易的方法来实现简单的对象迭代,相比较定义类实现Iterator接口的方式,性能开销和复杂性大大降低。
PHP官方文档这样说的:
生成器允许你在foreach代码块中写代码来迭代一组数据而不需要在内存中创建一个数组,那会使你的内存达到上限,或者会占据可观的处理时间。相反,你可以写一个生成器函数,就像一个普通的自定义函数一样,和普通函数只返回一次不同的是,生成器可以根据需要yield多次,以便生成需要迭代的值。
一个简单的例子就是使用生成器来重新实现range()函数。标准的range()函数需要在内存中生成一个数组包含每一个在它范围内的值,然后返回该数组,结果就是会产生多个很大的数组。比如,调用range(0,1000000)将导致内存占用超过100MB。
做为一种替代方法,我们可以实现一个xrange()生成器,只需要足够的内存来创建Iterator对象并在内部跟踪生成器的当前状态,这样只需要不到1K字节的内存。
官方文档给了上文对应的例子,我们在此简化了一下:
functionxrange($start,$limit,$step=1){ for($i=$start;$i<=$limit;$i+=$step){ yield$i+1=>$i;//关键字yield表明这是一个generator } } //我们可以这样调用 foreach(xrange(0,10,2)as$key=>$value){ printf("%d%d\n",$key,$value); }
可能你已经发现了,这个例子的输出和我们前面在说迭代器的时候那个例子结果一样。实际上生成器生成的正是一个迭代器对象实例,该迭代器对象继承了Iterator接口,同时也包含了生成器对象自有的接口,具体可以参考Generator类的定义。
当一个生成器被调用的时候,它返回一个可以被遍历的对象.当你遍历这个对象的时候(例如通过一个foreach循环),PHP将会在每次需要值的时候调用生成器函数,并在产生一个值之后保存生成器的状态,这样它就可以在需要产生下一个值的时候恢复调用状态。
一旦不再需要产生更多的值,生成器函数可以简单退出,而调用生成器的代码还可以继续执行,就像一个数组已经被遍历完了。
我们需要注意的关键是yield,这是生成器的关键。我们通过上面例子,可以看得出,yield会将当前一个值传递给foreach,换句话说,foreach每一次迭代过程都会从yield处取一个值,直到整个遍历过程不再存在yield为止的时候,遍历结束。
我们也可以发现,yield和return都会返回值,但区别在于一个return是返回既定结果,一次返回完毕就不再返回新的结果,而yield是不断产出直到无法产出为止。
实际上存在yield的函数返回值返回的是一个Generator对象(这个对象不能手动通过new实例化),该对象实现了Iterator接口。那么Generator自身有什么独特之处?继续看:
yield
字面上解释,yield代表着让位、让行。正是这个让行使得通过yield实现协程变得可能。
生成器函数的核心是yield关键字。它最简单的调用形式看起来像一个return申明,不同之处在于普通return会返回值并终止函数的执行,而yield会返回一个值给循环调用此生成器的代码并且只是暂停执行生成器函数。
yield和return的区别,前者是暂停当前过程的执行并返回值,而后者是中断当前过程并返回值。暂停当前过程,意味着将处理权转交由上一级继续进行,直至上一级再次调用被暂停的过程,该过程则会从上一次暂停的位置继续执行。这像是什么呢?如果读者在读本篇文章之前已经在鸟哥的文章中粗略看过,应该知道这很像是一个操作系统的进程调度管理,多个进程在一个CPU核心上执行,在系统调度下每一个进程执行一段指令就被暂停,切换到下一个进程,这样看起来就像是同时在执行多个任务。
但仅仅是如此还远远不够,yield更重要的特性是除了可以返回一个值以外,还能够接收一个值!
functionprinter() { while(true){ printf("receive:%s\n",yield); } } $printer=printer(); $printer->send('hello'); $printer->send('world');
上述例子输出内容为:
receive:hello
receive:world
参考PHP官方中文文档:生成器对象我们可以得知Generator对象除了实现Iterator接口中的必要方法以外,还有一个send方法,这个方法就是向yield语句处传递一个值,同时从yied语句处继续执行,直至再次遇到yield后控制权回到外部。
我们通过之前也了解了一个问题,yield可以在其位置中断并返回一个值,那么能不能同时进行接收和返回呢?当然,这可是实现协程的根本。我们对上述代码做出修改:
current()); $printer->send('hello'); printf("%d\n",$printer->current()); $printer->send('world'); printf("%d\n",$printer->current());
输出内容如下:
1
receive:hello
2
receive:world
3
current方法是迭代器(Iterator)接口必要的方法,foreach语句每一次迭代都会通过其获取当前值,而后调用迭代器的next方法。我们为了使程序不会无限执行,手动调用current方法获取值。
上述例子已经足以表示yield在那一个位置作为双向传输的工具,已具备实现协程的条件。
协程
这一部分我不打算长篇大论,本文开头已经给出了鸟哥博客中更为完善的文章,本文的目的是出于补充对Generator的细节。
我们要知道,对于单核处理器,多任务的执行原理是让每一个任务执行一段时间,然后中断、让另一个任务执行然后在中断后执行下一个,如此反复。由于其执行切换速度很快,让外部认为多个任务实际上是“并行”的。
鸟哥那篇文章这么说道:
多任务协作这个术语中的“协作”很好的说明了如何进行这种切换的:它要求当前正在运行的任务自动把控制传回给调度器,这样就可以运行其他任务了。这与“抢占”多任务相反,抢占多任务是这样的:调度器可以中断运行了一段时间的任务,不管它喜欢还是不喜欢。协作多任务在Windows的早期版本(windows95)和MacOS中有使用,不过它们后来都切换到使用抢先多任务了。理由相当明确:如果你依靠程序自动交出控制的话,那么一些恶意的程序将很容易占用整个CPU,不与其他任务共享。
我们结合之前的例子,可以发现,yield作为可以让一段任务自身中断,然后回到外部继续执行。利用这个特性可以实现多任务调度的功能,配合yield的双向通讯功能,以实现任务和调度器之间进行通信。
这样的功能对于读写和操作Stream资源时尤为重要,我们可以极大的提高程序对于并发流资源的处理能力,比如实现tcpserver。以上在《PHP中使用协同程序实现合作多任务》有更为详尽的例子。本文不再赘述。
总结
PHP自5.4到如今愈发稳定的PHP7,可以看到许多的新特性令这门语言愈发强大和完善,逐渐从纯粹的Web语言变得有着更为广泛的适用面,作为一枚PHPer的确不应当止步不前,我们依然有很多的东西需要不断学习和加强。
虽然“PHP是世界上最好的语言”这句话只是个调侃,但不可否认PHP即使不是最好,但也在努力变好的事实,对吧?
更多关于PHP相关内容感兴趣的读者可查看本站专题:《php常用函数与技巧总结》、《php字符串(string)用法总结》、《PHP数组(Array)操作技巧大全》、《PHP数据结构与算法教程》及《php程序设计算法总结》
希望本文所述对大家PHP程序设计有所帮助。