上一篇文章,我们讲到了协程的意义,以及它出现是为了解决哪些痛点。也知道了,协程其实相当于在用户空间,实现一个操作系统的线程调度器。当然从某种意义上说,和操作系统的调度器也不太一样。不过总的来说需要实现以下功能:1.在代码的某一行挂起。2.在某个时间阶段再从刚才挂起的地方拉起,继续向下执行。说白了就是保存现场和恢复现场。在汇编层面,这就比较简单了,保存各个寄存器的值,等需要的时候恢复就可以了。剩下的就是一下内存空间的管理,这里就先不管。
总之,如果要实现切换,必须要深入到底层去操作。而PHP作为一个上层的语言,可以说操作底层比较麻烦。除非PHP虚拟机给你提供了相关功能。
在python中,我知道有一个yield语法可以实现函数在某个地方中断,在从某个地方拉起运行。结果后来居然发现PHP也有这个语法,不愧为世界上最好的语言(逃
认识yield
yield是PHP生成器的一个语法。yield和return一样,都会返回数据,但是最大的不同点在于return会结束掉整个函数,下一次再执行,会从头开始。比如<?php
function retFunc(){
return true;
echo "hello world";
}
retFunc();
retFunc();
retFunc();
这个代码,无论retFunc()这个函数调用多少次,“hello world”永远不会输出出来,因为在打印之前就已经return了,表示函数已经结束。
那如果情况是下面这样<?php
function yieldFunc(){
yield true;
echo "hello world";
yield true;
echo "hello world again";
}
yieldFunc();
yieldFunc();
yieldFunc();
再一次我们用yield来代替return。试试看,诶??好像还是没有任何值打印出来?
这是因为生成器的调用和普通的函数调用不一样。
我们用var_dump来看看他们之间的区别var_dump( yieldFunc() );
var_dump( retFunc() );
你会发现,输出如下信息object(Generator)#1 (0) {
}
bool(true)
可以看到,retFunc函数返回了一个bool型数据,这个数据是我们return的时候的数据。而yieldFunc并没有返回bool型数据(虽然我们yield true;),而是返回一个Object(Generator)这是一个生成器对象。也就是说,你每一次调用yieldFunc()都是返回一个生成器对象。
如果要执行这个生成器,要按照生成器对象自己的规则来。
使用yield
好了,那么问题来了,我们知道现在函数返回了一个生成器,接下来我们要去学会如何使用它。就像你花大价钱买了一个榴莲回家,结果不知道怎么打开榴莲,那就太浪费了。如果你在生活中发生这种事情,千万要冷静,不能惊慌。用袋子把榴莲包起来装好,马上打电话给我,我会去办你解决掉这个大烦恼(斜眼笑)。
最简单的方法:foreach
PHP的foreach能处理这个迭代器。毕竟foreach本来就是处理循环迭代用的。
假设现在我们有一个yield的函数function yieldNum(){
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
}
然后我们就可以使用foreach来调用它foreach (yieldNum() as $num) {
echo $num, "n";
}
最后会输出1
2
3
4
5
哦,对了,简单讲一下,yield后面的值可以简单理解为每一次return回的数据。只不过这个‘return’和之前的函数return不一样。
使用Generator对象
因为yield函数返回的是一个生成器Generator对象,所有。自然应该使用原生的Generator对象去操作。
对象如下Generator implements Iterator {
/* Methods */
public current ( void ) : mixed //获取当前yield的值
public getReturn ( void ) : mixed //获取函数的return值
public key ( void ) : mixed
public next ( void ) : void //唤醒生成器执行
public rewind ( void ) : void //重置迭代器
public send ( mixed $value ) : mixed //中途发送数据给yield
public throw ( Throwable $exception ) : mixed
public valid ( void ) : bool //验证是否已经被关闭了
public __wakeup ( void ) : void
}
然后我们用生成器来调用一下我们刚才的那个yieldNum函数。写起来就像下面这这个样子,确实没有foreach来的简洁。但好处是它更灵活,便于我们以后对生成器进行操作。$g = yieldNum(); //获取生成器
while($g->valid()){ //看看是否还可用
echo $g->current()."n"; //获取当前的yield返回的值
$g->next(); //移到下一步
}
与之交互
如果仅仅能实现在函数的某个位置中断,显然还不够。我们希望不仅中断,还能在中断恢复的时候与之进行交互。也就是说,我们希望,下一次启动的时候,我们可以自己传一些值给他。
如果我们仔细看一下Generator对象的实现,会发现有一个send方法。这个方法就是可以在函数再次启动的时候,赋值给它。
我们来一个例子,假设有一个奇怪的函数BlackBox,它是一个生成器,每一次都会随机输出一个0~100的数字function BlackBox(){
while(true){
yield mt_rand(0,100);
}
}
然后我们调用它$g = BlackBox();
echo $g->current()."n";
$g->next();
echo $g->current()."n";
会发现,打印出了2个随机数37
65
好了,然后我们现在有一个需求,希望我们可以随意调整最大值,就是说,再一次我们希望返回(0,100)的随机数,下一次我希望这个函数返回(0,1000)的随机数。这要怎么去实现呢?这就是send方法的用武之地了。
我们重新修改一下我们的代码function BlackBox(){
$max = 100;
while(true){
$temp = (yield mt_rand(0,$max));
if($temp != null){
$max = $temp;
}
}
}
先来简单解释一下这个代码。第一声明了一个$max变量,用于存储最大值,默认为100。然后不断循环。接着代码执行到yield mt_rand(0,$max)这个语句,程序会调用mt_rand生成一个(0,$max)的随机数,并暂停然后返回随机数。
接下来的$temp = (yield mt_rand(0,$max));才是重头戏,右边由括号包裹,表示等代码重新启动运行的时候,右边的表达式会返回一个值(有send方法传输过去的),这个值会保存到变量$temp里面。
如果temp变量里的值是null,则表示send方法没有传输值,又或者使用的是next方法。
题外话,next方法其实就是send(null)的一种实现
最后,判断一下temp变量里的值是否为null,如果不为空,则表示send了最大值,把这个值赋值给max变量,接下来再执行一次循环,代码暂停在yield mt_rand(0,$max)。等待下一次调用current方法去获取值。
然后我们来看看调用方法$g = BlackBox();
echo $g->current()."n";
$g->next();
echo $g->current()."n";
$g->send(10000);
echo $g->current()."n";
程序返回8
53
1574
可以很明显的看出来,第一次第二次的随机数都是在100以内的,而第三次的随机数是10000以内的。我们的代码成功了!
总结
OK,以上就是yield简单的使用方法。了解yield对我们进行了的文章很有帮助,因为我们的协程就是需要用到yield的这种特型来实现。如果有兴趣可以看看yield相关的教程。接下来的文章我们会继续走我们的PHP协程之路,让我们拭目以待吧~
注意:本文未经许可,不得转载