這篇文章運(yùn)用簡單易懂的例子給大家介紹PHP中yield的使用方法,代碼非常詳細(xì),感興趣的小伙伴們可以參考借鑒,希望對大家能有所幫助。
成都創(chuàng)新互聯(lián)公司-專業(yè)網(wǎng)站定制、快速模板網(wǎng)站建設(shè)、高性價(jià)比豐城網(wǎng)站開發(fā)、企業(yè)建站全套包干低至880元,成熟完善的模板庫,直接使用。一站式豐城網(wǎng)站制作公司更省心,省錢,快速模板網(wǎng)站建設(shè)找我們,業(yè)務(wù)覆蓋豐城地區(qū)。費(fèi)用合理售后完善,十多年實(shí)體公司更值得信賴。
剛開始接觸PHP
的 yield
的時(shí)候,感覺,yield
是什么黑科技,百度一下:yield
——協(xié)程,生成器。很多文章都在講 Iterator
,Generater
, 蛤~,這東西是 PHP 迭代器的一個(gè)補(bǔ)充。再翻幾頁,就是Go 協(xié)程
。我出于好奇點(diǎn)開看了下Go 協(xié)程
, 里面都是 并發(fā)
,線程
,管道通訊
這類字眼,wc,nb, 這tm才是黑科技啊,再回來看PHP
,分分鐘想轉(zhuǎn) Go
。
yield
語法是在版本5.5加入PHP
的,配合迭代器使用,功能上就是 流程控制
代碼,和goto
,return
類似。
以下就是官方提供的 yield 小例子,通過執(zhí)行結(jié)果,我們可分析當(dāng)代碼執(zhí)行到 yield $i
時(shí),他會進(jìn)行 return $i
, 待 echo "$value\n"
后, goto
for ($i = 1; $i <= 3; $i++) {
, 對!PHP 的 yield 就是一個(gè)能出能進(jìn)的語法。在z代碼中七進(jìn)七出,把 $i
平平安安得送了出來。
我們遇到了什么問題
寫代碼就是解決問題。我們來看看他們遇到了什么問題:php官方呢,需要言簡意賅地把yield介紹給大家。一部分網(wǎng)友呢,需要在有限的資源內(nèi)完成大文件操作。而我們的鳥哥。面對的一群對當(dāng)下yield的教程停留于初級而不滿意的phper,就以一個(gè)任務(wù)調(diào)度器作為例子,給大家講了一種
yield
高級用法。php.net:生成器語法,
PHP如何讀取大文件,
風(fēng)雪之隅:在PHP中使用協(xié)程實(shí)現(xiàn)多任務(wù)調(diào)度.提出問題,再用
yield
來解答,看到以上答案,我覺得呢,這PHP協(xié)程不過如此(和Go協(xié)程
相比 )。有句話——
一個(gè)好問題比答案更重要
,目前廣大網(wǎng)友還沒有給yield提出更好,更困難的問題。
yield
這個(gè)進(jìn)進(jìn)出出的語法,很多舉例都是再讓yield做迭代器啊,或者利用低內(nèi)存讀取超大文本的Excel
,csv
什么的,再高級就是用它實(shí)現(xiàn)一個(gè)簡單的任務(wù)調(diào)度器,并且這個(gè)調(diào)度器,一看代碼都差不多。我來出道題
正如一個(gè)好的問題,比答案更有價(jià)值
- 用PHP實(shí)現(xiàn)一個(gè) Socket Server,他能接收請求,并返回Server的時(shí)間。
好,這是第一個(gè)問題,鋪墊。 官方答案
- 在原來的代碼上,我們加個(gè)需求,該Socket Server 處理請求時(shí),依賴其他 Socket Server,還需要有 Client 功能。也就是他能接收請求,向其它Server發(fā)起請求。
這是第二個(gè)問題,也是鋪墊。
- 原來的Socket Server同一時(shí)間只能服務(wù)一個(gè)客戶,希望能實(shí)現(xiàn)一個(gè)
非阻塞I/O
Socket Server, 這個(gè) Server 內(nèi)有 Socket Client 功能,支持并發(fā)處理收到的請求,和主動發(fā)起的請求。要求不用多線程,多進(jìn)程。這個(gè)問題,還是鋪墊,這幾個(gè)問題很干,大家可以想一想,2,3題的答案,都放在一個(gè)腳本里了:nio_server.php
以上這段代碼,我列舉了一個(gè)具體的業(yè)務(wù),就是用戶請求購物車加購動作, 而購物車服務(wù)呢,又需要和 產(chǎn)品服務(wù),庫存服務(wù),優(yōu)惠服務(wù) 交互,來驗(yàn)證加購動作可行性。有同步,異步方式請求,并做對比。
后續(xù)還有很多代碼,我都放gitee鏈接了。使用方法,見readme.md
- 最后一個(gè)問題:在PHP中,用同步寫代碼,程序呢異步執(zhí)行?需要怎么調(diào)整代碼。
提示:這個(gè)和
PHP
的yield
語法有關(guān)。再提示:
yield
語法特征是什么,進(jìn)進(jìn)出出!看著我們的代碼,同步, 異步,進(jìn)進(jìn)出出你想到了什么?
看到代碼,同步處理模式下,這三個(gè)函數(shù)
checkInventory
checkProduct
checkPromo
時(shí),發(fā)起請求,并依次等待返回的結(jié)果,這三個(gè)函數(shù)執(zhí)行后,再響應(yīng)客戶請求。異步處理模式下,這三個(gè)函數(shù)發(fā)起請求完畢后,代碼就跳出循環(huán)了,然后是在
select()
下的一個(gè)代碼分支中接收請求, 并收集結(jié)果。每次收到結(jié)果后判斷是否完成,完成則響應(yīng)客戶端。那么能不能這樣:在異步處理的流程中,當(dāng)
Server
收到 自己發(fā)起的client
有數(shù)據(jù)響應(yīng)后,代碼跳到 nio_server.php 的 247行呢,這樣我們的收到請求校驗(yàn)相關(guān)的代碼就能放到這里,編碼能就是同步,容易理解。不然,client
的響應(yīng)處理放在 280 行以后,不通過抓包,真的很難理解,執(zhí)行了第 247 行代碼后,緊接著是從 280 行開始的。誒~這里是不是有 進(jìn)進(jìn)出出那種感覺了~ 代碼從 247 行出去,開始監(jiān)聽發(fā)出
Client
響應(yīng),收到返回?cái)?shù)據(jù),帶著數(shù)據(jù)再回到 247 行,繼續(xù)進(jìn)行邏輯校驗(yàn),綜合結(jié)果后,再響應(yīng)給客戶端。用yield來解決問題
基于 yield 實(shí)現(xiàn)的,同步編碼,
"異步"I/O
的Socket Server
就實(shí)現(xiàn)了。代碼。這里 “異步” 打了引號,大佬別扣這個(gè)字眼了。 該是
非阻塞I/O
不等大家的答案了,先上我的結(jié)果代碼吧,代碼呢都放在這個(gè)目錄下了。
gitee https://gitee.com/xupaul/PHP-generator-yield-Demo/tree/master/yield-socket
運(yùn)行測試代碼
clone 代碼到本地后,需要拉起4個(gè) command 命令程序:
拉起3個(gè)第三方服務(wù)
## 啟動一個(gè)處理耗時(shí)2s的庫存服務(wù)$ php ./other_server.php 8081 inventory 2## 啟動一個(gè)處理耗時(shí)4s的產(chǎn)品服務(wù)$ php ./other_server.php 8082 product 4## 監(jiān)聽8083端口,處理一個(gè)請求 耗時(shí)6s的 promo 服務(wù)$ php ./other_server.php 8083 promo 6啟動購物車服務(wù)
## 啟動一個(gè)非阻塞購物車服務(wù)$ php ./async_cart_server.php ## 或者啟動一個(gè)一般購物車服務(wù)$ php ./cart_server.php發(fā)起用戶請求
$ php ./user_client.php運(yùn)行結(jié)果呢如下,通過執(zhí)行的時(shí)間日志,可得這三個(gè)請求是并發(fā)發(fā)起的,不是阻塞通訊。
在看我們的代碼,三個(gè)函數(shù),發(fā)起
socket
請求,沒有設(shè)置callback
,而是通過yield from
接收了三個(gè)socket
的返回結(jié)果。也就是達(dá)到了,同步編碼,異步執(zhí)行的效果。
運(yùn)行結(jié)果
非阻塞模式
client 端日志:
通過以上
起始時(shí)間
和結(jié)束時(shí)間
,就看到這三個(gè)請求耗時(shí)總共就6s,也就按照耗時(shí)最長的promo服務(wù)的耗時(shí)來的。也就是說三個(gè)第三方請求都是并發(fā)進(jìn)行的。cart server 端日志:
而 cart 打印的日志,可以看到三個(gè)請求一并發(fā)起,并一起等待結(jié)果返回。達(dá)到非阻塞并發(fā)請求的效果。
阻塞模式
client 端日志:
以上是阻塞方式請求,可以看到耗時(shí) 12s。也就是三個(gè)服務(wù)加起來的耗時(shí)。
cart server 端日志:
cart 服務(wù),依次阻塞方式請求第三方服務(wù),順序執(zhí)行完畢后,共耗時(shí)12s,當(dāng)然如果第一個(gè),獲第二個(gè)服務(wù)報(bào)錯(cuò)的話,會提前結(jié)束這個(gè)檢查。會節(jié)約一點(diǎn)時(shí)間。
工作原理
這里就是用到了
yield
的工作特點(diǎn)——進(jìn)進(jìn)出出,在發(fā)起非阻塞socket
請求后,不是阻塞方式等待socket響應(yīng),而是使用yield
跳出當(dāng)前執(zhí)行生成器,等待有socket響應(yīng)后,在調(diào)用生成器的send
方法回到發(fā)起socket
請求的函數(shù)內(nèi),在yield from Async::all()
接收數(shù)據(jù)響應(yīng)數(shù)據(jù)搜集完畢后,返回。和Golang比一比
考慮到網(wǎng)速原因,我這就放上一個(gè)國內(nèi)教程鏈接:Go 并發(fā) 教程
php
的協(xié)程是真協(xié)程,而Go
是披著協(xié)程外衣的輕量化線程(“協(xié)程”里,都玩上“鎖”了,這就是線程)。我個(gè)人偏愛,協(xié)程的,覺得線程的調(diào)度有一定隨機(jī)性,因此需要鎖機(jī)制來保證程序的正確,帶來了額外開銷。協(xié)程的調(diào)度(換入換出)交給了用戶,保證了一段代碼執(zhí)行連續(xù)性(當(dāng)然進(jìn)程級上,還是會有換入換出的,除非是跨進(jìn)程的資源訪問,或者跨機(jī)器的資源訪問,這時(shí),就要用到分布式鎖了,這里不展開討論),同步編碼,異步執(zhí)行,只需要考慮那個(gè)哪個(gè)方法會有IO交互會協(xié)程跳出即可。
和NodeJS比劃一下
Javascript 和 PHP 兩個(gè)腳本語言有很多相似的地方,弱類型,動態(tài)對象,單線程,在Web領(lǐng)域生態(tài)豐富。不同的是,
Javascript
在瀏覽器端一開始就是異步的(如果js發(fā)起網(wǎng)絡(luò)請求只能同步進(jìn)行,那么你的網(wǎng)頁渲染線程會卡?。?,例如Ajax
,setTimeout
,setInterval
,這些都是異步+回調(diào)的方式工作。基于V8引擎而誕生的
NodeJS
,天生就是異步的,在提供高性能網(wǎng)絡(luò)服務(wù)有很大的優(yōu)勢,不過它的IO編碼范式
么。。。剛開始是 回調(diào)——?dú)У舻鬲z,后來有了Promise——屏幕豎起來看,以及Generator
——遇事不絕yield
一下吧,到現(xiàn)在的Async/Await
——語法糖?真香!可以說JS的委員非常勤快,在異步編程范式的標(biāo)準(zhǔn)制定也做的很好(以前我嘗試寫
NodeJS
時(shí),幾個(gè)回調(diào)就直接把我勸退了),2009年誕生的NodeJS
有點(diǎn)后來居上的意思。目前PHP
只是趕上了協(xié)程,期待PHP的Async/Await
語法糖的實(shí)現(xiàn)吧。PHP yield 使用注意事項(xiàng)
一旦使用上 yield 后,就必須注意調(diào)用函數(shù)是,會得到函數(shù)結(jié)果,還是 生成器對象。PHP 不會自動幫你區(qū)別,需要你手動代碼判斷結(jié)果類型——
if ($re instanceof \Generator) {}
, 如果你得到的是 生成器,但不希望去手動調(diào)用 current() 去執(zhí)行它,那么在生成器前 使用 yield from 交給上游(框架)來解決。爆改 Workerman
博客寫到這,就開始手癢癢了,看到Workerman框架,我在基礎(chǔ)上二開,使其能——同步編碼,異步執(zhí)行。
代碼已放到:PaulXu-cn/CoWorkerman.git
目前還是dev階段,大家喜歡可以先 體驗(yàn)一波。
$ composer require paulxu-cn/co-workerman一個(gè)簡單的單線程 TCP Server
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();這里設(shè)置fork 一個(gè)
worker
線程,處理邏輯中帶有一個(gè)sleep()
2s
的操作,依然不影響他同時(shí)響應(yīng)多個(gè)請求。啟動測試程序
## 啟動CoWorker服務(wù)$ php ./examples/example2/coWorkermanServer.php start## 啟動請求線程$ php ./examples/example2/userClientFork.php運(yùn)行結(jié)果
綠色箭頭——新的請求,紅色箭頭——響應(yīng)請求
從結(jié)果上看到,這一個(gè)worker線程,在接收新的請求同時(shí),還在回復(fù)之前的請求,各個(gè)連接交錯(cuò)運(yùn)行。而我們的代碼呢,看樣子就是同步的,沒有回調(diào)。
CoWorker購物車服務(wù)
好的,這里我們做幾個(gè)簡單的微服務(wù)模擬實(shí)際應(yīng)用,這里模擬
用戶請求端
,購物車服務(wù)
,庫存服務(wù)
,產(chǎn)品服務(wù)
。 模擬用戶請求加購動作,購物車去分別請求 庫存,產(chǎn)品 校驗(yàn)用戶是否可以加購,并響應(yīng)客戶請求是否成功。代碼我就不貼了,太長了,麻煩移步 CoWorkerman/example/example5/coCartServer.php
運(yùn)行命令
## 啟動庫存服務(wù)$ php ./examples/example5/otherServerFork.php 8081 inventory 1## 啟動產(chǎn)品服務(wù)$ php ./examples/example5/otherServerFork.php 8082 product 2## 啟動CoWorker 購物車服務(wù)$ php ./examples/example5/coCartServer.php start## 用戶請求端$ php ./examples/example5/userClientFork.php運(yùn)行結(jié)果
黃色箭頭——新的用戶請求,藍(lán)色箭頭——購物車發(fā)起庫存,產(chǎn)品檢查請求,紅色箭頭——響應(yīng)用戶請求
從圖中看到也是用1個(gè)線程服務(wù)多個(gè)連接,交錯(cuò)運(yùn)行。
好的,那么PHP
CoWorkerman
也能像NodeJS
那樣用Async/Await
那樣同步編碼,異步運(yùn)行了。快來試試這個(gè) CoWorkerman 吧:
$ composer require paulxu-cn/co-workerman工作原理
workerman
內(nèi)的worker進(jìn)程
遇到阻塞函數(shù)的處理方式時(shí),會等待IO返回,如果這個(gè)時(shí)候,又有了新的請求,那么閑的worker會競爭到這個(gè)新的連接。我在上圖worker5中,描述了一個(gè)
AsyncTCPConnection
使用情況,woker內(nèi)發(fā)起了一個(gè)非阻塞請求,并注冊了回調(diào)函數(shù),然后程序繼續(xù)運(yùn)行到結(jié)束。當(dāng)異步請求響應(yīng)時(shí),就需要通過其他方式去響應(yīng)(如自己再發(fā)起一個(gè)請求告知請求方)。在下圖中
CoWorkerman
,也是多個(gè)Worker競爭新的請求,當(dāng)worker1收到一個(gè)新的請求,會產(chǎn)生一個(gè)生成器,生成器內(nèi)發(fā)起異步請求,并注冊響應(yīng)回調(diào),請求響應(yīng)后,回到該生成器跳出(yield
)的地方,繼續(xù)執(zhí)行代碼。發(fā)起異步請求,并注冊回調(diào)函數(shù),這些默認(rèn)工作
CoWorkerman
框架內(nèi)已做了,回調(diào)函數(shù)內(nèi)工作是:收到數(shù)據(jù),并發(fā)給 發(fā)起該請求的生成器。這例子中,通過調(diào)用 Promise:all() 發(fā)起多個(gè)請求,并監(jiān)聽結(jié)果返回,待所有的響應(yīng)返回再繼續(xù)運(yùn)行生成器
在程序
yield
跳出后,該worker就處于事件循環(huán)狀態(tài)($event->loop()
),也就是多路監(jiān)聽:請求端口,第三方客戶端請求響應(yīng)端口。這個(gè)時(shí)候如果:
- 有新的請求來,他和其他
worker
競爭新的請求,如果競爭到了,則該worker內(nèi)又產(chǎn)生一個(gè)新的 生成器。- 客戶端有響應(yīng),則調(diào)用回調(diào)函數(shù)
- 客戶端都響應(yīng)了,繼續(xù)運(yùn)行 生成器程序。
從1中,我們可假設(shè),如果就一個(gè)
Worker
,那么該Worker
可以在上一個(gè)請求未完成情況下,繼續(xù)接受處理下一個(gè)請求。也就是CoWorkerman
可以在單Worker
下運(yùn)行,并發(fā)處理多個(gè)請求。當(dāng)然,這里也有個(gè)前提,單
Worker
模式內(nèi)不能運(yùn)行阻塞函數(shù),一旦阻塞,后續(xù)請求就會堵在網(wǎng)卡。所以,除非對自己的代碼非常了解,如果用到第三方庫,那么我還是建議你在多Worker
模式下運(yùn)行CoWorkerman
,阻塞時(shí),還有其他Worker
兜住新請求。CoWorkerman 的意義
- 用同步的代碼,發(fā)起異步請求,多個(gè)請求可并發(fā),從IO串行等待,改為并行等待,減少無畏的等待時(shí)間。提高業(yè)務(wù)程序的效率同時(shí),不降低代碼可讀性。
- 在一個(gè)線程內(nèi)通過事件循環(huán),盡可能處理多個(gè)請求,緩解了一個(gè)請求一個(gè)線程帶來的頻繁線程切換,從核心上提高運(yùn)行效率。
CoWorkerman 生態(tài)位
適合處理純
Socket
請求的應(yīng)用,如Workerman Gateway
,或者是大前端
整合多個(gè)服務(wù)RPC
結(jié)果, 綜合后返給前三頁
這樣的場景.日志記錄是每個(gè)程序最基本需求,由于寫文件函數(shù)是阻塞的,建議用消息隊(duì)列,或者redis隊(duì)列,更或者跳過
Logstash
直接丟Elasticsearch
.CoWorkerman有他的局限性,也有他自己位置。
關(guān)于PHP中yield的使用方法就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,可以學(xué)到更多知識。如果覺得文章不錯(cuò),可以把它分享出去讓更多的人看到。
文章標(biāo)題:PHP中yield的使用方法
標(biāo)題網(wǎng)址:http://weahome.cn/article/jcipop.html