PHP回顾之协程
转载请注明文章出处: https://tlanyan.me/php-review...
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/gene...; - PHP7新增了
yield from
语法,实现了生成器委托,详情请参考其RFC: https://wiki.php.net/rfc/gene...; - 生成器是单向迭代器,开动后不能调用
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/lang...
- http://php.net/manual/zh/clas...
- https://wiki.php.net/rfc/gene...
- https://wiki.php.net/rfc/gene...
- https://zhuanlan.zhihu.com/p/...
- http://www.laruence.com/2015/...
- https://medium.com/async-php/...
- https://blog.kghost.info/2011...
PHP回顾之协程的更多相关文章
- Day10 - Python协程、异步IO、redis缓存、rabbitMQ队列
Python之路,Day9 - 异步IO\数据库\队列\缓存 本节内容 Gevent协程 Select\Poll\Epoll异步IO与事件驱动 Python连接Mysql数据库操作 RabbitM ...
- Python协程、异步IO
本节内容 Gevent协程 Select\Poll\Epoll异步IO与事件驱动 Python连接Mysql数据库操作 RabbitMQ队列 Redis\Memcached缓存 Paramiko SS ...
- 异步IO/协程/数据库/队列/缓存(转)
原文:Python之路,Day9 - 异步IO\数据库\队列\缓存 作者:金角大王Alex add by zhj: 文章很长 引子 到目前为止,我们已经学了网络并发编程的2个套路, 多进程,多线程,这 ...
- Python之协程、异步IO、redis缓存、rabbitMQ队列
本节内容 Gevent协程 Select\Poll\Epoll异步IO与事件驱动 Python连接Mysql数据库操作 RabbitMQ队列 Redis\Memcached缓存 Paramiko SS ...
- python---基础知识回顾(十)进程和线程(协程gevent:线程在I/O请求上的优化)
优点:使用gevent协程,可以更好的利用线程资源.(基于线程实现) 需求:使用一个线程,去请求多个网站的资源(注意,请求上会有延时)<实际上是去请求了大量的网站信息,我们使用了多线程,只不过每 ...
- 协程--gevent模块(单线程高并发)
先恶补一下知识点,上节回顾 上下文切换:当CPU从执行一个线程切换到执行另外一个线程的时候,它需要先存储当前线程的本地的数据,程序指针等,然后载入另一个线程的本地数据,程序指针等,最后才开始执行.这种 ...
- python之协程与IO操作
协程 协程,又称微线程,纤程.英文名Coroutine. 协程的概念很早就提出来了,但直到最近几年才在某些语言(如Lua)中得到广泛应用. 子程序,或者称为函数,在所有语言中都是层级调用,比如A调用B ...
- Python之路【第七篇续】:进程、线程、协程
Socket Server模块 SocketServer内部使用 IO多路复用 以及 “多线程” 和 “多进程” ,从而实现并发处理多个客户端请求的Socket服务端.即:每个客户端请求连接到服务器时 ...
- Python 中的进程、线程、协程、同步、异步、回调
进程和线程究竟是什么东西?传统网络服务模型是如何工作的?协程和线程的关系和区别有哪些?IO过程在什么时间发生? 一.上下文切换技术 简述 在进一步之前,让我们先回顾一下各种上下文切换技术. 不过首先说 ...
随机推荐
- 打造终端下mutt收发邮件环境(fbterm,fetchmail,msmtp,procmail,mutt)
实现mutt下收发邮件须要安装,mutt,fbterm,fetchmail,msmtp,procmail 下面是各配置文件.在home文件夹下,隐私信息有马赛克... .muttrc : 当中Mail ...
- sql处理高并发
sql处理高并发,防止库存超卖 2014-08-14 23:44 13560人阅读 评论(2) 收藏 举报 分类: 数据库(43) 今天王总又给我们上了一课,其实mysql处理高并发,防止库存超卖 ...
- 关于move_uploaded_file()出错的问题
move_upload0ed_file()函数返回參数较少.可是引起出错的原因却有非常多,所以对于刚開始学习的人难免会遇到问题. 出错原因大概有下面三点: 1.假设检測到文件不是来自post上传.这个 ...
- Android5.1开机LOGO与开机动画【转】
本文转载自:http://blog.csdn.net/u014770862/article/details/52624627 android5.1中,开机LOGO部分和之前版本的并不相同,主要区别在于 ...
- [计蒜客] tsy's number 解题报告 (莫比乌斯反演+数论分块)
interlinkage: https://nanti.jisuanke.com/t/38226 description: solution: 显然$\frac{\phi(j^2)}{\phi(j)} ...
- iOS开发中UIDatePicker控件的使用方法简介
iOS上的选择时间日期的控件是这样的,左边是时间和日期混合,右边是单纯的日期模式. 您可以选择自己需要的模式,Time, Date,Date and Time , Count Down Timer四 ...
- NOIP 2013 T2 火柴排队 ---->求逆序对
[NOIP2013T2]火柴排队 背景 noip2013day1 描述 涵涵有两盒火柴,每盒装有 n 根火柴,每根火柴都有一个高度. 现在将每盒中的火柴各 自 排成一列, 同一列火柴的高度互不相同, ...
- B - Calculating Function
Problem description For a positive integer n let's define a function f: f(n) = - 1 + 2 - 3 + .. + ( ...
- 让break跳出外层循环的方法
demo //在里层循环里面,想办法让外层循环的条件不成立,就可以控制外层循环结束. for(var i = 0 ; i < 10; i++){ alert(i) for(var j = 0 ; ...
- 在webstrorm中配置好es6 babel【更新:在webstorm中配置.vue和.vue文件中支持es6】
第一步:全局安装babel-cli npm install -g babel-cli 第二步,新建一个空项目,在 WebStorm 中的当前项目中打开 Terminal,进入项目的根目录下, 安装 E ...