转载请注明文章出处: https://tlanyan.me/php-review...
PHP自5.5起引入了生成器(Generator),基于其可实现协程编程。本文先回顾生成器,而后过渡到协程编程。php
生成器是一种数据类型,实现了iterator
接口。不能经过new
获得生成器实例,也没有获取生成器实例的静态方法。获得生成器实例的惟一办法是调用生成器函数(包含yield
关键字的函数)。调用生成器函数直接返回一个生成器对象,生成器运行时函数内的代码才开始执行。html
先上代码直观感觉一下yield
与生成器:git
# 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
函数内的代码执行,脚本结束。github
如其名,生成器能够用来生成数据。只是其生成数据的方式与其余函数不同:生成器经过yield
返回数据,而非return
; yield
返回数据后,生成器函数不会销毁,只是暂停运行,将来能够从暂停处恢复运行;生成器运行一次,(只)返回一个数据,屡次运行就返回多个数据;不调用生成器获取数据,生成器内的代码就躺着不动,所谓动次打次,说的就是生成器生成数据的样子。golang
生成器实现了迭代器接口,获取生成器数据能够用foreach
循环或手工current/next/valid
。以下代码演示数据生成和遍历:web
# 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
语句会暂停执行,让出CPU并将控制权返回到调用者,下次执行时从中断点继续执行。控制权返回到调用者时,yield
语句能够携带值返回给调用方。generator2.php
脚本演示了yield返回值的三种形式:数据库
yield
让函数能够随时暂停、继续执行,并返回数据给调用方。若是继续执行时须要外部数据,这个工做由生成器的send
函数提供:出如今yield
左边等号的变量会接收send
传来的值。看一个常见的send
函数使用样例:express
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)
;return
值,PHP7后能够return值,并经过生成器的getReturn
获取返回的值。详情参考返回值的RFC:https://wiki.php.net/rfc/gene...;yield from
语法,实现了生成器委托,详情请参考其RFC: https://wiki.php.net/rfc/gene...;rewind
。相对于其余迭代器,生成器具备性能开销小、编码容易的特色。其做用主要体如今三个方面:promise
关于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协程编程中应当注意的事项。
感谢阅读,欢迎指正!