本文轉載於SegmentFault社區
作者:小白要生髮
開篇
剛開始接觸PHP 的 yield 的時候,感覺,yield 是什麼黑科技,百度一下:yield——協程,生成器。很多文章都在講 Iterator ,Generater, 蛤~,這東西是 PHP 疊代器的一個補充。再翻幾頁,就是Go 協程。我出於好奇點開看了下Go 協程, 裡面都是 並發,線程,管道通訊這類字眼,wc,nb, 這tm才是黑科技啊,再回來看PHP,分分鐘想轉 Go。
四部曲
yield 語法探究
yield from 語法探究
yield 實戰「多線程」編碼
PHP yield 高級用法 網絡——終章
yield 語法加入 PHP
yield語法是在版本5.5加入PHP的,配合疊代器使用,功能上就是 流程控制 代碼,和goto,return 類似。
以下就是官方提供的 yield 小例子,通過執行結果,我們可分析當代碼執行到 yield $i 時,他會進行 return $i, 待 echo "$valuen" 後, goto for ($i = 1; $i <= 3; $i++) {, 對!PHP 的 yield 就是一個能出能進的語法。在z代碼中七進七出,把 $i 平平安安得送了出來。
functiongen_one_to_three{ for($i = 1; $i <= 7; $i++) { //注意變量$i的值在不同的yield之間是保持傳遞的。yield$i; }}
$generator = gen_one_to_three;foreach($generator as$value) { echo"$valuen"; }
// output12...67
我們遇到了什麼問題
寫代碼就是解決問題。我們來看看他們遇到了什麼問題:php官方呢,需要言簡意賅地把yield介紹給大家。一部分網友呢,需要在有限的資源內完成大文件操作。而我們的鳥哥。面對的一群對當下yield的教程停留於初級而不滿意的phper,就以一個任務調度器作為例子,給大家講了一種yield高級用法。
php.net:生成器語法:
https://www.php.net/manual/zh/language.generators.syntax.php
PHP如何讀取大文件:
https://www.luyuqiang.com/how-php-read-a-large-file
風雪之隅:在PHP中使用協程實現多任務調度:
https://www.laruence.com/2015/05/28/3038.html
提出問題,再用yield來解答,看到以上答案,我覺得呢,這PHP協程不過如此(和Go協程相比 )。
有句話——一個好問題比答案更重要,目前廣大網友還沒有給yield提出更好,更困難的問題。
yield這個進進出出的語法,很多舉例都是再讓yield做疊代器啊,或者利用低內存讀取超大文本的Excel,csv什麼的,再高級就是用它實現一個簡單的任務調度器,並且這個調度器,一看代碼都差不多。
我來出道題
正如一個好的問題,比答案更有價值
用PHP實現一個 Socket Server,他能接收請求,並返回Server的時間。
好,這是第一個問題,鋪墊。 官方答案:
https://www.php.net/manual/zh/function.stream-socket-server.php
在原來的代碼上,我們加個需求,該Socket Server 處理請求時,依賴其他 Socket Server,還需要有 Client 功能。也就是他能接收請求,向其它Server發起請求。
這是第二個問題,也是鋪墊。
原來的Socket Server同一時間只能服務一個客戶,希望能實現一個 非阻塞I/O Socket Server, 這個 Server 內有 Socket Client 功能,支持並發處理收到的請求,和主動發起的請求。要求不用多線程,多進程。
這個問題,還是鋪墊,這幾個問題很乾,大家可以想一想,2,3題的答案,都放在一個腳本里了:nio_server.php
https://gitee.com/xupaul/php-nio-server/blob/master/cart-server-demo/nio_server.php
後續還有很多代碼,我都放gitee連結了。使用方法,見readme.md
最後一個問題:在PHP中,用同步寫代碼,程序呢異步執行?需要怎麼調整代碼。
提示:這個和 PHP的 yield語法有關。
再提示:yield 語法特徵是什麼,進進出出!
看著我們的代碼,同步, 異步,進進出出 你想到了什麼?
看到代碼,同步處理模式下,這三個函數checkInventory checkProduct checkPromo 時,發起請求,並依次等待返回的結果,這三個函數執行後,再響應客戶請求。
異步處理模式下,這三個函數發起請求完畢後,代碼就跳出循環了,然後是在select下的一個代碼分支中接收請求, 並收集結果。每次收到結果後判斷是否完成,完成則響應客戶端。
那麼能不能這樣:在異步處理的流程中,當 Server 收到 自己發起的 client 有數據響應後,代碼跳到 nio_server.php的 247行呢,這樣我們的收到請求校驗相關的代碼就能放到這裡,編碼能就是同步,容易理解。不然,client 的響應處理放在 280 行以後,不通過抓包,真的很難理解,執行了第 247 行代碼後,緊接著是從 280 行開始的。
誒~這裡是不是有 進進出出 那種感覺了~ 代碼從 247 行出去,開始監聽發出 Client 響應,收到返回數據,帶著數據再回到 247 行,繼續進行邏輯校驗,綜合結果後,再響應給客戶端。
用yield來解決問題
基於 yield 實現的,同步編碼,"異步"I/O 的 Socket Server 就實現了。代碼。
這裡 "異步" 打了引號,大佬別扣這個字眼了。該是非阻塞I/O
不等大家的答案了,先上我的結果代碼吧,代碼呢都放在這個目錄下了。
giteehttps://gitee.com/xupaul/PHP-generator-yield-Demo/tree/master/yield-socket
運行測試代碼
clone 代碼到本地後,需要拉起4個 command 命令程序:
拉起3個第三方服務
## 啟動一個處理耗時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 接收數據響應數據搜集完畢後,返回。
和Golang比一比
考慮到網速原因,我這就放上一個國內教程連結:Go 並發 教程(https://www.runoob.com/go/go-concurrent.html)
php的協程是真協程,而Go是披著協程外衣的輕量化線程(「協程」里,都玩上「鎖」了,這就是線程)。
我個人偏愛,協程的,覺得線程的調度有一定隨機性,因此需要鎖機制來保證程序的正確,帶來了額外開銷。協程的調度(換入換出)交給了用戶,保證了一段代碼執行連續性(當然進程級上,還是會有換入換出的,除非是跨進程的資源訪問,或者跨機器的資源訪問,這時,就要用到分布式鎖了,這裡不展開討論),同步編碼,異步執行,只需要考慮那個哪個方法會有IO交互會協程跳出即可。
和NodeJS比劃一下
Java 和 PHP 兩個腳本語言有很多相似的地方,弱類型,動態對象,單線程,在Web領域生態豐富。不同的是,Java在瀏覽器端一開始就是異步的(如果js發起網絡請求只能同步進行,那麼你的網頁渲染線程會卡住),例如Ajax,setTimeout,setInterval,這些都是異步+回調的方式工作。
基於V8引擎而誕生的NodeJS,天生就是異步的,在提供高性能網絡服務有很大的優勢,不過它的IO編碼範式麼。。。剛開始是 回調——毀掉地獄,後來有了Promise——螢幕豎起來看,以及Generator——遇事不絕yield一下吧,到現在的Async/Await——語法糖?真香!
可以說JS的委員非常勤快,在異步編程範式的標準制定也做的很好(以前我嘗試寫NodeJS時,幾個回調就直接把我勸退了),2009年誕生的NodeJS有點後來居上的意思。目前PHP只是趕上了協程,期待PHP的Async/Await語法糖的實現吧。
PHP yield 使用注意事項
一旦使用上 yield 後,就必須注意調用函數是,會得到函數結果,還是 生成器對象。PHP 不會自動幫你區別,需要你手動代碼判斷結果類型—— if ($re instanceof Generator) {}, 如果你得到的是 生成器,但不希望去手動調用 current 去執行它,那麼在生成器前 使用 yield from 交給上游(框架)來解決。
爆改 Workerman
博客寫到這,就開始手痒痒了,看到Workerman框架,我在基礎上二開,使其能——同步編碼,異步執行。
代碼已放到:PaulXu-cn/CoWorkerman.git
目前還是dev階段,大家喜歡可以先 體驗一波。
$ composer requirepaulxu-cn/co-workerman
一個簡單的單線程 TCP Server
// file: ./examples/example2/coWorkermanServer.php , 詳細代碼見github$worker = newCoWorker( 'tcp://0.0.0.0:8080'); // 設置fork一個子進程$worker->count = 1;
$worker->onConnect = function(CoTcpConnection $connection){ try{ $conName = "{$connection->getRemoteIp}:{$connection->getRemotePort}"; echoPHP_EOL . "New Connection, {$conName} n";
$re = yieldfrom $connection->readAsync( 1024); CoWorker::safeEcho( 'get request msg :'. $re . PHP_EOL );
yieldfrom 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線程,在接收新的請求同時,還在回復之前的請求,各個連接交錯運行。而我們的代碼呢,看樣子就是同步的,沒有回調。
CoWorker購物車服務
好的,這裡我們做幾個簡單的微服務模擬實際應用,這裡模擬 用戶請求端,購物車服務,庫存服務,產品服務。模擬用戶請求加購動作,購物車去分別請求 庫存,產品 校驗用戶是否可以加購,並響應客戶請求是否成功。
代碼我就不貼了,太長了,麻煩移步
CoWorkerman/example/example5/coCartServer.php:
https://github.com/PaulXu-cn/CoWorkerman/blob/master/examples/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 requirepaulxu-cn/co-workerman
總結
自此 PHP 協程編碼到 網絡異步編碼就到此結束了,如果看到這邊文章有很多疑惑,或者對 yield 語法不了解的,那麼讀一讀這個系列前幾篇文章打打基礎是非常有必要。
https://github.com/PaulXu-cn/CoWorkerman
參考
https://ericfu.me/several-ways-to-aync/
https://segmentfault.com/a/1190000018106557
SegmentFault 思否社區和文章作者展開更多互動和交流。