本文先介紹了生成器的概念,重點是yield的用法及生成器的介面。協程部分則簡單說了協程的原理,以及PHP協程編程中應注意的事項。
PHP自5.5起引進了生成器(Generator),基於其可實現協程程式設計。本文先回顧生成器,然後再過渡到協程程式設計。
生成器是一種資料類型,實作了iterator介面。不能透過new得到生成器實例,也沒有取得生成器實例的靜態方法。得到生成器實例的唯一方法是呼叫生成器函數(包含yield關鍵字的函數)。呼叫生成器函數直接傳回一個生成器對象,生成器運行時函數內的程式碼才開始執行。
先上程式碼直覺地感受一下yield與生成器:
# 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
函數內的程式碼執行,腳本結束。
如其名,生成器可以用來產生資料。只是其生成數據的方式與其他函數不一樣:生成器通過yield
返回數據,而非return
; yield
返回數據後,生成器函數不會銷毀,只是暫停運行,未來可以從暫停處恢復運行;生成器運行一次,(只)返回一個數據,多次運行就返回多個數據;不調用生成器獲取數據,生成器內的代碼就躺著不動,所謂動次打次,說的就是生成器產生資料的樣子。
生成器實作了迭代器接口,取得生成器資料可以用foreach
循環或手動current/next/valid
。如下程式碼示範資料產生和遍歷:
# 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 $key => $value: 傳回資料的key和value;
yield $value: 傳回數據,key由系統指派;
yield: 傳回null值,key由系統指派;
##yield
讓函數可以隨時暫停、繼續執行,並傳回資料給呼叫方。如果繼續執行時需要外部數據,這個工作由生成器的send
函數提供:出現在yield
左邊等號的變數會接收send
傳來的值。看一個常見的send
函數使用範例:
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)
;
PHP5生成器函數不能return
值,PHP7後可以return值,並且經過生成器的 getReturn
取得傳回的值。
PHP7新增了yield from
語法,實作了生成器委託。
生成器是單向迭代器,開啟後無法呼叫rewind
。
相對於其他迭代器,生成器具有效能開銷小、編碼容易的特性。其角色主要體現在三個面向:
資料產生(生產者),透過yield回傳資料;
資料消費(消費者),消費send傳來的資料;
實現協程。
關於PHP中的生成器及基本用法,建議看看 2gua 大佬的博文:PHP之生成器,生動有趣且易懂。
協程(coroutine)是隨時可中斷、恢復執行的子程序,yield
關鍵字讓函數擁有這種能力,所以可以用於協程編程。
執行緒歸屬於行程,一個行程可有多個執行緒。進程是電腦分配資源的最小單位,執行緒是電腦調度執行的最小單位。行程和執行緒均由作業系統調度。
協程可以看成“用戶狀態的執行緒”,需要使用者程式實現調度。執行緒和進程由作業系統調度「搶佔式」交替運行,協程主動讓出CPU「協商式」交替運行。協程十分的輕量,協程切換不涉及執行緒切換,執行效率高,數目越多,越能體現協程的優勢。
生成器實現的協程屬於無堆疊協程(stackless coroutine),即生成器函數只有函數幀,運行時附加到呼叫方的棧上執行。有別於功能強大的有棧協程(stackful coroutine),生成器暫停後無法控製程式走向,只能將控制權被動的歸還呼叫者;生成器只能中斷自身,不能中斷整個協程。當然,生成器的好處便是效率高(暫停時只需儲存程式計數器即可),實作簡單。
說到PHP中的協程編程,相信大部分人已經看過鳥哥轉載(翻譯)的這篇博文:在PHP中使用協程實現多任務調度。原文作者 nikic 是PHP的核心開發者,生成器功能的倡議者和實作人。想深入了解生成器及基於其的協程編程,nikic關於生成器的RFC和鳥哥網站上的文章必讀。
先看看基於生成器的協程工作方式:協程協作式工作,即協程之間透過主動讓出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上很多,本文不再給出。
相關推薦:
PHP中的output_buffering詳細介紹,outputbuffering_PHP教學
#以上是php中協程的詳細介紹(程式碼)的詳細內容。更多資訊請關注PHP中文網其他相關文章!