PHP yield 高級用法 網絡

2020-06-20     segmentfault官方

原標題:PHP yield 高級用法 網絡

本文轉載於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什麼的,再高級就是用它實現一個簡單的任務調度器,並且這個調度器,一看代碼都差不多。

我來出道題

正如一個好的問題,比答案更有價值

  1. 用PHP實現一個 Socket Server,他能接收請求,並返回Server的時間。

好,這是第一個問題,鋪墊。 官方答案:

https://www.php.net/manual/zh/function.stream-socket-server.php

  1. 在原來的代碼上,我們加個需求,該Socket Server 處理請求時,依賴其他 Socket Server,還需要有 Client 功能。也就是他能接收請求,向其它Server發起請求。

這是第二個問題,也是鋪墊。

  1. 原來的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

  1. 最後一個問題:在PHP中,用同步寫代碼,程序呢異步執行?需要怎麼調整代碼。

提示:這個和 PHPyield語法有關。

再提示: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 思否社區和文章作者展開更多互動和交流。

文章來源: https://twgreatdaily.com/zh-mo/j16o0HIBd4Bm1__YlsXz.html

Flutter 知識點

2020-08-10