剛開始接觸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
謝謝!