PHP回顾系列目录
- PHP基础
- web请求
- cookie
- web响应
- session
- 数据库操作
- 加解密
- Composer
- 创建自己的Composer包
- 发送邮件
- IO
- 流
- Socket编程
- 多进程编程
- 执行流程及相关概念
PHP自5.5起引入了生成器(Generator),基于其可实现协程编程。本文先回顾生成器,然后过渡到协程编程。
yield与生成器
生成器
生成器是一种数据类型,实现了iterator
接口。不能通过new
得到生成器实例,也没有获取生成器实例的静态方法。得到生成器实例的唯一办法是调用生成器函数(包含yield
关键字的函数)。调用生成器函数直接返回一个生成器对象,生成器运行时函数内的代码才开始执行。
先上代码直观感受一下yield
与生成器:
# generator1.php
function foo() {
exit('exit script when generator runs.');
yield;
}
$gen = foo();
var_dump($gen);
$gen->current();
echo 'unreachable code!';
# 执行结果
object(Generator)#1 (0) {
}
exit script when generator runs.
foo
函数包含yield
关键字,变身为生成器函数。调用foo
不会执行函数体中的任何代码,而是返回一个生成器实例。生成器运行后,foo
函数内的代码执行,脚本结束。
如其名,生成器可以用来生成数据。只是其生成数据的方式与其他函数不一样:生成器通过yield
返回数据,而非return
; yield
返回数据后,生成器函数不会销毁,只是暂停运行,未来可以从暂停处恢复运行;生成器运行一次,(只)返回一个数据,多次运行就返回多个数据;不调用生成器获取数据,生成器内的代码就躺着不动,所谓动次打次,说的就是生成器生成数据的样子。
生成器实现了迭代器接口,获取生成器数据可以用foreach
循环或手工current/next/valid
。如下代码演示数据生成和遍历:
# generator2.php
function foo() {
# 返回键值对数据
yield "key1" => "value1";
$count = 0;
while ($count < 5) {
# 返回值,key自动生成
yield $count;
++ $count;
}
# 不返回值,相当于返回null
yield;
}
# 手动获取生成器数据
$gen = foo();
while ($gen->valid()) {
fwrite(STDOUT, "key:{$gen->key()}, value:{$gen->current()}\n");
$gen->next();
}
# foreach 遍历数据
fwrite(STDOUT, "\ndata from foreach\n");
foreach (foo() as $key => $value) {
fwrite(STDOUT, "key:$key, value:$value\n");
}
yield
yield
关键字是生成器的核心,其让普通函数异化(进化)为生成器函数。yield
有“让出”的意思,程序执行到yield
语句会暂停执行,让出CPU并将控制权返回到调用者,下次执行时从中断点继续执行。控制权返回到调用者时,yield
语句可以携带值返回给调用方。generator2.php
脚本演示了yield返回值的三种形式:
- yield $key => $value: 返回数据的key和value;
- yield $value: 返回数据,key由系统分配;
- yield: 返回null值,key由系统分配;
yield
让函数可以随时暂停、继续执行,并返回数据给调用方。如果继续执行时需要外部数据,这个工作由生成器的send
函数提供:出现在yield
左边等号的变量会接收send
传来的值。看一个常见的send
函数使用样例:
function logger(string $filename) {
$fd = fopen($filename, 'w+');
while($msg = yield) {
fwrite($fd, date('Y-m-d H:i:s') . ':' . $msg . PHP_EOL);
}
fclose($fd);
}
$logger = logger('log.txt');
$logger->send('program starts!');
// do some thing
$logger->send('program ends!');
send
让生成器之间和外部有双向数据通信的能力:yield
返回数据;send
提供继续运行的支撑数据。由于send
让生成器继续执行,这个行为与迭代器的next
接口类似,next
相当于send(null)
。
其他
$string = yield $data;
的表达式在PHP7前不合法,需要加括号:$string = (yield $data)
;- PHP5生成器函数不能
return
值,PHP7后可以return值,并通过生成器的getReturn
获取返回的值。详情参考返回值的RFC:https://wiki.php.net/rfc/generator-return-expressions; - PHP7新增了
yield from
语法,实现了生成器委托,详情请参考其RFC: https://wiki.php.net/rfc/generator-delegation; - 生成器是单向迭代器,开动后不能调用
rewind
。
总结
相对于其他迭代器,生成器具有性能开销小、编码容易的特点。其作用主要体现在三个方面:
- 数据生成(生产者),通过yield返回数据;
- 数据消费(消费者),消费send传来的数据;
- 实现协程。
关于PHP中的生成器及基本用法,建议看看 2gua 大佬的博文:PHP之生成器,生动有趣且易懂。
协程编程
协程(coroutine)是随时可中断、恢复执行的子程序,yield
关键字让函数拥有这种能力,所以可以用于协程编程。
进程、线程和协程
线程归属于进程,一个进程可有多个线程。进程是计算机分配资源的最小单位,线程是计算机调度执行的最小单位。进程和线程均由操作系统调度。
协程可以看成“用户态的线程”,需要用户程序实现调度。线程和进程由操作系统调度“抢占式”交替运行,协程主动让出CPU“协商式”交替运行。协程十分的轻量,协程切换不涉及线程切换,执行效率高,数目越多,越能体现协程的优势。
生成器和协程
生成器实现的协程属于无栈协程(stackless coroutine),即生成器函数只有函数帧,运行时附加到调用方的栈上执行。不同于功能强大的有栈协程(stackful coroutine),生成器暂停后无法控制程序走向,只能将控制权被动的归还调用者;生成器只能中断自身,不能中断整个协程。当然,生成器的好处便是效率高(暂停时只需保存程序计数器即可),实现简单。
协程编程
说到PHP中的协程编程,相信大部分人已经看过鸟哥转载(翻译)的这篇博文:在PHP中使用协程实现多任务调度。原文作者 nikic 是PHP的核心开发者,生成器功能的倡议者和实现人。想深入了解生成器及基于其的协程编程,nikic关于生成器的RFC和鸟哥网站上的文章必读。
nikic的文章,生成器部分好懂,看完后用yield
写个xrange
类似函数肯定毫无压力。为什么一进入协程,就有点懵逼呢?
先看看基于生成器的协程工作方式:协程协作式工作,即协程之间通过主动让出CPU达到多任务交替运行(即并发多任务,但不是并行);一个生成器可看成一个协程,执行到yield
语句,让出CPU控制权回到调用方,调用方继续执行其他协程或其他代码。
再来看鸟哥博客理解的难点何在。协程非常轻量,一个系统中可以同时存在成千上万个协程(生成器)。而操作系统不会对协程调度,安排协程执行的工作就落到开发者身上。部分人看不懂鸟哥文章的协程部分,是因为里面说协程编程少(写协程主要就是写生成器函数),而是花笔墨实现了一个协程的调度器(scheduler或者kernel):模拟了操作系统,对所有协程进行公平调度。PHP开发一般的思维是:我写了这些代码,PHP引擎会调用我这些代码得到预期结果。而协程编程不仅要写干活的代码,还要写指导这些代码什么时候干活的代码。没有很好的把握作者的思维,理解起来自然会难一些。需要自行调度,这是生成器协程相对于原生协程(async/await形式)的一个缺点。
知道了协程是怎么回事,那么它能用来干什么?协程自行让出CPU来协作高效利用CPU,让出的时机当然应该是程序阻塞时。什么地方会让程序阻塞呢?用户态的代码鲜有阻塞,阻塞主要是系统调用。而系统调用的大头是IO,所以协程的主要应用场景在网络编程。为了让程序高性能、高并发,程序应该异步执行不能阻塞。既然异步执行,就需要通知和回调,写回调函数避免不了“回调地狱(callback hell)”的问题:代码可读性差,程序执行流程散落在层层回调函数中等。解决回调地狱的方式主要有两种:Promise和协程。协程能以同步的方式编写代码,在高性能网络编程(IO密集型)中是推荐的。
再回过头看PHP中的协程编程。PHP中基于生成器实现实现协程编程,优先推荐使用RecoilPHP
、Amp
等协程框架。这些框架已经写好了调度器,在其上开发直接写生成器函数,内核会自动调度执行(想让一个函数以协程方式调度执行,在函数体内加上yield
即可)。如果不想用yield
方式进行协程编程,推荐swoole
或其衍生框架,能做到类似golang的协程编程体验,又能享受PHP的开发效率。
如果想用原生态的做PHP协程编程,类似鸟哥博客中的调度器必不可少。调度器调度协程执行,协程中断后控制权又回到调度器中。所以调度器应该总是在主(事件)循环中,即CPU不在执行协程,就应当在执行调度器的代码。无协程运行时,调度器应当自我阻塞避免消耗CPU(鸟哥博客中使用了内置的select
系统调用),等待事件到来再执行相应的协程。程序运行期间,除了调度器阻塞,协程在运行过程中不应该调用阻塞API。
总结
在协程编程中,yield
的主要作用是将控制权转让,无需纠结于其返回值(基本上yield
返回的值会在下次执行时直接send
过来)。重点应当关注控制权转让的时机,以及协程的运作方式。
另外需要说明一点,协程和异步没有多大关系,还要看运行环境支撑。常规的PHP运行环境,即使用了promise/coroutine,也还是同步阻塞的。再牛逼的协程框架,sleep
一下也不好使了。作为类比,即使JavaScript不使用promise/async这些技术,也是异步非阻塞的。
通过生成器和Promise,能实现类似于await
的协程编程,相关代码在Github上很多,本文不再给出。
总结
本文先介绍了生成器的概念,重点是yield
的用法及生成器的接口。协程部分则简要说了协程的原理,以及PHP协程编程中应当注意的事项。
感谢阅读,欢迎指正!
参考
- http://php.net/manual/zh/language.generators.php
- http://php.net/manual/zh/class.generator.php
- https://wiki.php.net/rfc/generators
- https://wiki.php.net/rfc/generator-return-expressions
- https://zhuanlan.zhihu.com/p/21329131
- http://www.laruence.com/2015/05/28/3038.html
- https://medium.com/async-php/co-operative-php-multitasking-ce4ef52858a0
- https://blog.kghost.info/2011/11/15/abstract-control-1-stackless-coroutine/
原文链接:https://www.qiquanji.com/post/4909.html
本站声明:网站内容来源于网络,如有侵权,请联系我们,我们将及时处理。
微信扫码关注
更新实时通知