PHP回顧之協程

轉載請註明文章出處: https://tlanyan.me/php-review...

PHP回顧系列目錄

PHP自5.5起引入了生成器(Generator),基於其可實現協程編程。本文先回顧生成器,而後過渡到協程編程。php

yield與生成器

生成器

生成器是一種數據類型,實現了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有「讓出」的意思,程序執行到yield語句會暫停執行,讓出CPU並將控制權返回到調用者,下次執行時從中斷點繼續執行。控制權返回到調用者時,yield語句能夠攜帶值返回給調用方。generator2.php腳本演示了yield返回值的三種形式:數據庫

  1. yield $key => $value: 返回數據的key和value;
  2. yield $value: 返回數據,key由系統分配;
  3. yield: 返回null值,key由系統分配;

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)編程

其餘

  1. $string = yield $data;的表達式在PHP7前不合法,須要加括號:$string = (yield $data);
  2. PHP5生成器函數不能return值,PHP7後能夠return值,並經過生成器的getReturn獲取返回的值。詳情參考返回值的RFC:https://wiki.php.net/rfc/gene...
  3. PHP7新增了yield from語法,實現了生成器委託,詳情請參考其RFC: https://wiki.php.net/rfc/gene...
  4. 生成器是單向迭代器,開動後不能調用rewind

總結

相對於其餘迭代器,生成器具備性能開銷小、編碼容易的特色。其做用主要體如今三個方面:promise

  1. 數據生成(生產者),經過yield返回數據;
  2. 數據消費(消費者),消費send傳來的數據;
  3. 實現協程。

關於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中基於生成器實現實現協程編程,優先推薦使用RecoilPHPAmp等協程框架。這些框架已經寫好了調度器,在其上開發直接寫生成器函數,內核會自動調度執行(想讓一個函數以協程方式調度執行,在函數體內加上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協程編程中應當注意的事項。

感謝閱讀,歡迎指正!

參考

  1. http://php.net/manual/zh/lang...
  2. http://php.net/manual/zh/clas...
  3. https://wiki.php.net/rfc/gene...
  4. https://wiki.php.net/rfc/gene...
  5. https://zhuanlan.zhihu.com/p/...
  6. http://www.laruence.com/2015/...
  7. https://medium.com/async-php/...
  8. https://blog.kghost.info/2011...
相關文章
相關標籤/搜索