刚开始接触PHP
的 yield
的时候,感受,yield
是什么黑科技,百度一下:yield
——协程,生成器。不少文章都在讲 Iterator
,Generater
, 蛤~,这东西是 PHP 迭代器的一个补充。再翻几页,就是Go 协程
。我出于好奇点开看了下Go 协程
, 里面都是 并发
,线程
,管道通信
这类字眼,wc,nb, 这tm才是黑科技啊,再回来看PHP
,分分钟想转 Go
。php
yield语法是在版本5.5加入PHP的,配合迭代器使用,功能上就是 流程控制
代码,和goto
,return
相似。html
如下就是官方提供的 yield 小例子,经过执行结果,咱们可分析当代码执行到 yield $i
时,他会进行 return $i
, 待 echo "$value\n"
后, goto
for ($i = 1; $i <= 3; $i++) {
, 对!PHP 的 yield 就是一个能出能进的语法。在z代码中七进七出,把 $i
平平安安得送了出来。git
<?php function gen_one_to_three() { for ($i = 1; $i <= 7; $i++) { //注意变量$i的值在不一样的yield之间是保持传递的。 yield $i; } } $generator = gen_one_to_three(); foreach ($generator as $value) { echo "$value\n"; } // output 1 2 ... 6 7
写代码就是解决问题。咱们来看看他们遇到了什么问题:php官方呢,须要言简意赅地把yield介绍给你们。一部分网友呢,须要在有限的资源内完成大文件操做。而咱们的鸟哥。面对的一群对当下yield的教程停留于初级而不满意的phper,就以一个任务调度器做为例子,给你们讲了一种yield
高级用法。github
php.net:生成器语法,
PHP如何读取大文件,
风雪之隅:在PHP中使用协程实现多任务调度.shell
提出问题,再用yield
来解答,看到以上答案,我以为呢,这PHP协程不过如此(和Go协程
相比 )。编程
有句话——一个好问题比答案更重要
,目前广大网友尚未给yield提出更好,更困难的问题。json
yield
这个进进出出的语法,不少举例都是再让yield作迭代器啊,或者利用低内存读取超大文本的Excel
,csv
什么的,再高级就是用它实现一个简单的任务调度器,而且这个调度器,一看代码都差很少。segmentfault
正如一个好的问题,比答案更有价值
好,这是第一个问题,铺垫。 官方答案浏览器
这是第二个问题,也是铺垫。网络
非阻塞I/O
Socket Server, 这个 Server 内有 Socket Client 功能,支持并发处理收到的请求,和主动发起的请求。要求不用多线程,多进程。这个问题,仍是铺垫,这几个问题很干,你们能够想想,2,3题的答案,都放在一个脚本里了:nio_server.php
以上这段代码,我列举了一个具体的业务,就是用户请求购物车加购动做, 而购物车服务呢,又须要和 产品服务,库存服务,优惠服务 交互,来验证加购动做可行性。有同步,异步方式请求,并作对比。
后续还有不少代码,我都放gitee连接了。使用方法,见readme.md
.
.
.
.
.
.
提示:这个和 PHP
的 yield
语法有关。
.
.
.
.
.
.
再提示:yield
语法特征是什么,进进出出!
看着咱们的代码,同步, 异步,进进出出 你想到了什么?
.
.
.
.
.
.
看到代码,同步处理模式下,这三个函数checkInventory
checkProduct
checkPromo
时,发起请求,并依次等待返回的结果,这三个函数执行后,再响应客户请求。
异步处理模式下,这三个函数发起请求完毕后,代码就跳出循环了,而后是在select()
下的一个代码分支中接收请求, 并收集结果。每次收到结果后判断是否完成,完成则响应客户端。
那么能不能这样:在异步处理的流程中,当 Server
收到 本身发起的 client
有数据响应后,代码跳到 nio_server.php 的 247行呢,这样咱们的收到请求校验相关的代码就能放到这里,编码能就是同步,容易理解。否则,client
的响应处理放在 280 行之后,不经过抓包,真的很难理解,执行了第 247 行代码后,紧接着是从 280 行开始的。
.
.
.
.
.
.
.
.
诶~这里是否是有 进进出出 那种感受了~ 代码从 247 行出去,开始监听发出 Client
响应,收到返回数据,带着数据再回到 247 行,继续进行逻辑校验,综合结果后,再响应给客户端。
基于 yield 实现的,同步编码,"异步"I/O
的 Socket Server
就实现了。代码。
这里 "异步" 打了引号,大佬别扣这个字眼了。 该是
非阻塞I/O
不等你们的答案了,先上个人结果代码吧,代码呢都放在这个目录下了。
gitee https://gitee.com/xupaul/PHP-generator-yield-Demo/tree/master/yield-socket
clone 代码到本地后,须要拉起4个 command 命令程序:
## 启动一个处理耗时2s的库存服务 $ php ./other_server.php 8081 inventory 2 ## 启动一个处理耗时4s的产品服务 $ php ./other_server.php 8082 product 4 ## 监听8083端口,处理一个请求 耗时6s的 promo 服务 $ php ./other_server.php 8083 promo 6
## 启动一个非阻塞购物车服务 $ php ./async_cart_server.php ## 或者启动一个通常购物车服务 $ php ./cart_server.php
$ php ./user_client.php
运行结果呢以下,经过执行的时间日志,可得这三个请求是并发发起的,不是阻塞通信。
在看咱们的代码,三个函数,发起socket
请求,没有设置callback
,而是经过yield from
接收了三个socket
的返回结果。
也就是达到了,同步编码,异步执行的效果。
client 端日志:
经过以上 起始时间
和 结束时间
,就看到这三个请求耗时总共就6s,也就按照耗时最长的promo服务的耗时来的。也就是说三个第三方请求都是并发进行的。
cart server 端日志:
而 cart 打印的日志,能够看到三个请求一并发起,并一块儿等待结果返回。达到非阻塞并发请求的效果。
client 端日志:
以上是阻塞方式请求,能够看到耗时 12s。也就是三个服务加起来的耗时。
cart server 端日志:
cart 服务,依次阻塞方式请求第三方服务,顺序执行完毕后,共耗时12s,固然若是第一个,获第二个服务报错的话,会提早结束这个检查。会节约一点时间。
这里就是用到了 yield
的工做特色——进进出出,在发起非阻塞socket
请求后,不是阻塞方式等待socket响应,而是使用yield
跳出当前执行生成器,等待有socket响应后,在调用生成器的send
方法回到发起socket
请求的函数内,在 yield from Async::all()
接收数据响应数据搜集完毕后,返回。
考虑到网速缘由,我这就放上一个国内教程连接:Go 并发 教程
php
的协程是真协程,而Go
是披着协程外衣的轻量化线程(“协程”里,都玩上“锁”了,这就是线程)。
我我的偏心,协程的,以为线程的调度有必定随机性,所以须要锁机制来保证程序的正确,带来了额外开销。协程的调度(换入换出)交给了用户,保证了一段代码执行连续性(固然进程级上,仍是会有换入换出的,除非是跨进程的资源访问,或者跨机器的资源访问,这时,就要用到分布式锁了,这里不展开讨论),同步编码,异步执行,只须要考虑那个哪一个方法会有IO交互会协程跳出便可。
Javascript 和 PHP 两个脚本语言有不少类似的地方,弱类型,动态对象,单线程,在Web领域生态丰富。不一样的是,Javascript
在浏览器端一开始就是异步的(若是js发起网络请求只能同步进行,那么你的网页渲染线程会卡住),例如Ajax
,setTimeout
,setInterval
,这些都是异步+回调的方式工做。
基于V8引擎而诞生的NodeJS
,天生就是异步的,在提供高性能网络服务有很大的优点,不过它的IO编码范式
么。。。刚开始是 回调——毁掉地狱,后来有了Promise——屏幕竖起来看,以及Generator
——遇事不绝yield
一下吧,到如今的Async/Await
——语法糖?真香!
能够说JS的委员很是勤快,在异步编程范式的标准制定也作的很好(之前我尝试写NodeJS
时,几个回调就直接把我劝退了),2009年诞生的NodeJS
有点后来居上的意思。目前PHP
只是遇上了协程,期待PHP的Async/Await
语法糖的实现吧。
一旦使用上 yield 后,就必须注意调用函数是,会获得函数结果,仍是 生成器对象。PHP 不会自动帮你区别,须要你手动代码判断结果类型—— if ($re instanceof \Generator) {}
, 若是你获得的是 生成器,但不但愿去手动调用 current() 去执行它,那么在生成器前 使用 yield from 交给上游(框架)来解决。
博客写到这,就开始手痒痒了,看到Workerman框架,我在基础上二开,使其能——同步编码,异步执行。
代码已放到:PaulXu-cn/CoWorkerman.git
目前仍是dev阶段,你们喜欢能够先 体验一波。
$ composer require paulxu-cn/co-workerman
<?php // file: ./examples/example2/coWorkermanServer.php , 详细代码见github $worker = new CoWorker('tcp://0.0.0.0:8080'); // 设置fork一个子进程 $worker->count = 1; $worker->onConnect = function (CoTcpConnection $connection) { try { $conName = "{$connection->getRemoteIp()}:{$connection->getRemotePort()}"; echo PHP_EOL . "New Connection, {$conName} \n"; $re = yield from $connection->readAsync(1024); CoWorker::safeEcho('get request msg :' . $re . PHP_EOL ); yield from CoTimer::sleepAsync(1000 * 2); $connection->send(json_encode(array('productId' => 12, 're' =>true))); CoWorker::safeEcho('Response to :' . $conName . PHP_EOL . PHP_EOL); } catch (ConnectionCloseException $e) { CoWorker::safeEcho('Connection closed, ' . $e->getMessage() . PHP_EOL); } }; CoWorker::runAll();
这里设置fork 一个worker
线程,处理逻辑中带有一个sleep()
2s
的操做,依然不影响他同时响应多个请求。
## 启动CoWorker服务 $ php ./examples/example2/coWorkermanServer.php start ## 启动请求线程 $ php ./examples/example2/userClientFork.php
绿色箭头——新的请求,红色箭头——响应请求
从结果上看到,这一个worker线程,在接收新的请求同时,还在回复以前的请求,各个链接交错运行。而咱们的代码呢,看样子就是同步的,没有回调。
好的,这里咱们作几个简单的微服务模拟实际应用,这里模拟 用户请求端
,购物车服务
,库存服务
,产品服务
。 模拟用户请求加购动做,购物车去分别请求 库存,产品 校验用户是否能够加购,并响应客户请求是否成功。
代码我就不贴了,太长了,麻烦移步 CoWorkerman/example/example5/coCartServer.php
## 启动库存服务 $ php ./examples/example5/otherServerFork.php 8081 inventory 1 ## 启动产品服务 $ php ./examples/example5/otherServerFork.php 8082 product 2
## 启动CoWorker 购物车服务 $ php ./examples/example5/coCartServer.php start
## 用户请求端 $ php ./examples/example5/userClientFork.php
黄色箭头——新的用户请求,蓝色箭头——购物车发起库存,产品检查请求,红色箭头——响应用户请求
从图中看到也是用1个线程服务多个链接,交错运行。
好的,那么PHP CoWorkerman
也能像 NodeJS
那样用 Async/Await
那样同步编码,异步运行了。
快来试试这个 CoWorkerman 吧:
$ composer require paulxu-cn/co-workerman
自此 PHP 协程编码到 网络异步编码就到此结束了,若是看到这边文章有不少疑惑,或者对 yield
语法不了解的,那么读一读这个系列前几篇文章打打基础是很是有必要。
若是行,请三连。CoWorkerman
谢谢!