PHP回顧之協程
轉載請註明文章出處: https://tlanyan.me/php-review...
PHP回顧系列目錄
- PHP基礎
- web請求
- cookie
- web響應
- session
- 資料庫操作
- 加解密
- Composer
- 建立自己的Composer包
- 傳送郵件
- IO
- 流
- Socket程式設計
- 多程序程式設計
- 執行流程及相關概念
PHP自5.5起引入了生成器(Generator),基於其可實現協程程式設計。本文先回顧生成器,然後過渡到協程程式設計。
yield與生成器
生成器
生成器是一種資料型別,實現了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
有“讓出”的意思,程式執行到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
獲取返回的值。詳情參考返回值的RFC:https://wiki.php.net/rfc/gene...; - PHP7新增了
yield from
語法,實現了生成器委託,詳情請參考其RFC: https://wiki.php.net/rfc/gene...; - 生成器是單向迭代器,開動後不能呼叫
rewind
。
總結
相對於其他迭代器,生成器具有效能開銷小、編碼容易的特點。其作用主要體現在三個方面:
- 資料生成(生產者),通過yield返回資料;
- 資料消費(消費者),消費send傳來的資料;
- 實現協程。
關於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協程程式設計中應當注意的事項。
感謝閱讀,歡迎指正!