轉載請註明文章出處: https://tlanyan.me/php-review...
PHP自5.5起引入了生成器(Generator),基於其可實現協程編程。本文先回顧生成器,而後過渡到協程編程。php
生成器是一種數據類型,實現了iterator
接口。不能經過new
獲得生成器實例,也沒有獲取生成器實例的靜態方法。獲得生成器實例的惟一辦法是調用生成器函數(包含yield
關鍵字的函數)。調用生成器函數直接返回一個生成器對象,生成器運行時函數內的代碼纔開始執行。html
先上代碼直觀感覺一下yield
與生成器:git
# generator1.php function foo() { exit('exit script when generator runs.'); yield; } $gen = foo(); var_dump($gen); $gen->current(); echo 'unreachable code!'; # 執行結果 object(Generator)#1 (0) { } exit script when generator runs.
foo
函數包含yield
關鍵字,變身爲生成器函數。調用foo
不會執行函數體中的任何代碼,而是返回一個生成器實例。生成器運行後,foo
函數內的代碼執行,腳本結束。github
如其名,生成器能夠用來生成數據。只是其生成數據的方式與其餘函數不同:生成器經過yield
返回數據,而非return
; yield
返回數據後,生成器函數不會銷燬,只是暫停運行,將來能夠從暫停處恢復運行;生成器運行一次,(只)返回一個數據,屢次運行就返回多個數據;不調用生成器獲取數據,生成器內的代碼就躺着不動,所謂動次打次,說的就是生成器生成數據的樣子。golang
生成器實現了迭代器接口,獲取生成器數據能夠用foreach
循環或手工current/next/valid
。以下代碼演示數據生成和遍歷:web
# generator2.php function foo() { # 返回鍵值對數據 yield "key1" => "value1"; $count = 0; while ($count < 5) { # 返回值,key自動生成 yield $count; ++ $count; } # 不返回值,至關於返回null yield; } # 手動獲取生成器數據 $gen = foo(); while ($gen->valid()) { fwrite(STDOUT, "key:{$gen->key()}, value:{$gen->current()}\n"); $gen->next(); } # foreach 遍歷數據 fwrite(STDOUT, "\ndata from foreach\n"); foreach (foo() as $key => $value) { fwrite(STDOUT, "key:$key, value:$value\n"); }
yield
關鍵字是生成器的核心,其讓普通函數異化(進化)爲生成器函數。yield
有「讓出」的意思,程序執行到yield
語句會暫停執行,讓出CPU並將控制權返回到調用者,下次執行時從中斷點繼續執行。控制權返回到調用者時,yield
語句能夠攜帶值返回給調用方。generator2.php
腳本演示了yield返回值的三種形式:數據庫
yield
讓函數能夠隨時暫停、繼續執行,並返回數據給調用方。若是繼續執行時須要外部數據,這個工做由生成器的send
函數提供:出如今yield
左邊等號的變量會接收send
傳來的值。看一個常見的send
函數使用樣例:express
function logger(string $filename) { $fd = fopen($filename, 'w+'); while($msg = yield) { fwrite($fd, date('Y-m-d H:i:s') . ':' . $msg . PHP_EOL); } fclose($fd); } $logger = logger('log.txt'); $logger->send('program starts!'); // do some thing $logger->send('program ends!');
send
讓生成器之間和外部有雙向數據通訊的能力:yield
返回數據;send
提供繼續運行的支撐數據。因爲send
讓生成器繼續執行,這個行爲與迭代器的next
接口相似,next
至關於send(null)
。編程
$string = yield $data;
的表達式在PHP7前不合法,須要加括號:$string = (yield $data)
;return
值,PHP7後能夠return值,並經過生成器的getReturn
獲取返回的值。詳情參考返回值的RFC:https://wiki.php.net/rfc/gene...;yield from
語法,實現了生成器委託,詳情請參考其RFC: https://wiki.php.net/rfc/gene...;rewind
。相對於其餘迭代器,生成器具備性能開銷小、編碼容易的特色。其做用主要體如今三個方面:promise
關於PHP中的生成器及基本用法,建議看看 2gua 大佬的博文:PHP之生成器,生動有趣且易懂。
協程(coroutine)是隨時可中斷、恢復執行的子程序,yield
關鍵字讓函數擁有這種能力,因此能夠用於協程編程。
線程歸屬於進程,一個進程可有多個線程。進程是計算機分配資源的最小單位,線程是計算機調度執行的最小單位。進程和線程均由操做系統調度。
協程能夠當作「用戶態的線程」,須要用戶程序實現調度。線程和進程由操做系統調度「搶佔式」交替運行,協程主動讓出CPU「協商式」交替運行。協程十分的輕量,協程切換不涉及線程切換,執行效率高,數目越多,越能體現協程的優點。
生成器實現的協程屬於無棧協程(stackless coroutine),即生成器函數只有函數幀,運行時附加到調用方的棧上執行。不一樣於功能強大的有棧協程(stackful coroutine),生成器暫停後沒法控制程序走向,只能將控制權被動的歸還調用者;生成器只能中斷自身,不能中斷整個協程。固然,生成器的好處即是效率高(暫停時只需保存程序計數器便可),實現簡單。
說到PHP中的協程編程,相信大部分人已經看過鳥哥轉載(翻譯)的這篇博文:在PHP中使用協程實現多任務調度。原文做者 nikic 是PHP的核心開發者,生成器功能的倡議者和實現人。想深刻了解生成器及基於其的協程編程,nikic關於生成器的RFC和鳥哥網站上的文章必讀。
nikic的文章,生成器部分好懂,看完後用yield
寫個xrange
相似函數確定毫無壓力。爲何一進入協程,就有點懵逼呢?
先看看基於生成器的協程工做方式:協程協做式工做,即協程之間經過主動讓出CPU達到多任務交替運行(即併發多任務,但不是並行);一個生成器可當作一個協程,執行到yield
語句,讓出CPU控制權回到調用方,調用方繼續執行其餘協程或其餘代碼。
再來看鳥哥博客理解的難點何在。協程很是輕量,一個系統中能夠同時存在成千上萬個協程(生成器)。而操做系統不會對協程調度,安排協程執行的工做就落到開發者身上。部分人看不懂鳥哥文章的協程部分,是由於裏面說協程編程少(寫協程主要就是寫生成器函數),而是花筆墨實現了一個協程的調度器(scheduler或者kernel):模擬了操做系統,對全部協程進行公平調度。PHP開發通常的思惟是:我寫了這些代碼,PHP引擎會調用我這些代碼獲得預期結果。而協程編程不只要寫幹活的代碼,還要寫指導這些代碼何時幹活的代碼。沒有很好的把握做者的思惟,理解起來天然會難一些。須要自行調度,這是生成器協程相對於原生協程(async/await形式)的一個缺點。
知道了協程是怎麼回事,那麼它能用來幹什麼?協程自行讓出CPU來協做高效利用CPU,讓出的時機固然應該是程序阻塞時。什麼地方會讓程序阻塞呢?用戶態的代碼鮮有阻塞,阻塞主要是系統調用。而系統調用的大頭是IO,因此協程的主要應用場景在網絡編程。爲了讓程序高性能、高併發,程序應該異步執行不能阻塞。既然異步執行,就須要通知和回調,寫回調函數避免不了「回調地獄(callback hell)」的問題:代碼可讀性差,程序執行流程散落在層層回調函數中等。解決回調地獄的方式主要有兩種:Promise和協程。協程能以同步的方式編寫代碼,在高性能網絡編程(IO密集型)中是推薦的。
再回過頭看PHP中的協程編程。PHP中基於生成器實現實現協程編程,優先推薦使用RecoilPHP
、Amp
等協程框架。這些框架已經寫好了調度器,在其上開發直接寫生成器函數,內核會自動調度執行(想讓一個函數以協程方式調度執行,在函數體內加上yield
便可)。若是不想用yield
方式進行協程編程,推薦swoole
或其衍生框架,能作到相似golang的協程編程體驗,又能享受PHP的開發效率。
若是想用原生態的作PHP協程編程,相似鳥哥博客中的調度器必不可少。調度器調度協程執行,協程中斷後控制權又回到調度器中。因此調度器應該老是在主(事件)循環中,即CPU不在執行協程,就應當在執行調度器的代碼。無協程運行時,調度器應當自我阻塞避免消耗CPU(鳥哥博客中使用了內置的select
系統調用),等待事件到來再執行相應的協程。程序運行期間,除了調度器阻塞,協程在運行過程當中不該該調用阻塞API。
在協程編程中,yield
的主要做用是將控制權轉讓,無需糾結於其返回值(基本上yield
返回的值會在下次執行時直接send
過來)。重點應當關注控制權轉讓的時機,以及協程的運做方式。
另外須要說明一點,協程和異步沒有多大關係,還要看運行環境支撐。常規的PHP運行環境,即便用了promise/coroutine,也仍是同步阻塞的。再牛逼的協程框架,sleep
一下也很差使了。做爲類比,即便JavaScript不使用promise/async這些技術,也是異步非阻塞的。
經過生成器和Promise,能實現相似於await
的協程編程,相關代碼在Github上不少,本文再也不給出。
本文先介紹了生成器的概念,重點是yield
的用法及生成器的接口。協程部分則簡要說了協程的原理,以及PHP協程編程中應當注意的事項。
感謝閱讀,歡迎指正!