首頁  >  文章  >  後端開發  >  php中協程的詳細介紹(程式碼)

php中協程的詳細介紹(程式碼)

不言
不言原創
2018-09-13 16:57:283892瀏覽

本文先介紹了生成器的概念,重點是yield的用法及生成器的介面。協程部分則簡單說了協程的原理,以及PHP協程編程中應注意的事項。

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傳回值的三種形式:

  1. yield $key => $value: 傳回資料的key和value;

  2. yield $value: 傳回數據,key由系統指派;

  3. 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)

其他

  1. $string = yield $data;的表達式在PHP7前不合法,需要加括號:$string = (yield $data);

  2. PHP5生成器函數不能return值,PHP7後可以return值,並且經過生成器的 getReturn取得傳回的值。

  3. PHP7新增了yield from語法,實作了生成器委託。

  4. 生成器是單向迭代器,開啟後無法呼叫rewind

總結

相對於其他迭代器,生成器具有效能開銷小、編碼容易的特性。其角色主要體現在三個面向:

  1. 資料產生(生產者),透過yield回傳資料;

  2. 資料消費(消費者),消費send傳來的資料;

  3. 實現協程。

關於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中基於生成器實作協程編程,優先推薦使用RecoilPHPAmp等協程框架。這些框架已經寫好了調度器,在其上開發直接寫入生成器函數,內核會自動調度執行(想讓一個函數以協程方式調度執行,在函數體內加上yield即可)。如果不想用yield方式進行協程編程,推薦swoole或其衍生框架,能做到類似golang的協程編程體驗,又能享受PHP的開發效率。

如果想用原始生態的做PHP協程編程,類似鳥哥博客中的調度器必不可少。調度器調度協程執行,協程中斷後控制權又回到調度器。所以調度器應該總是在主(事件)循環中,也就是CPU不在執行協程,就應該是執行調度器的程式碼。無協程運作時,調度器應當自我阻塞避免消耗CPU(鳥哥部落格中使用了內建的select系統呼叫),等待事件到來再執行對應的協程。程式運行期間,除了調度器阻塞,協程在運行過程中不應該呼叫阻塞API。

總結

在協程程式設計中,yield的主要作用是將控制權轉讓,無須糾結於其傳回值(基本上yield傳回的值會在下次執行時直接send過來)。重點應關注控制權移轉的時機,以及協程的運作方式。

另外需要說明一點,協程和非同步沒有太大關係,還要看運行環境支撐。常規的PHP運作環境,即使用了promise/coroutine,也仍是同步阻塞的。再屌的協程框架,sleep一下也不好使了。作為類比,即使JavaScript不使用promise/async這些技術,也是非同步非阻塞的。

透過生成器和Promise,能實現類似await的協程編程,相關程式碼在Github上很多,本文不再給出。

相關推薦:

PHP 中$_SERVER 詳細介紹

PHP中的output_buffering詳細介紹,outputbuffering_PHP教學

#

以上是php中協程的詳細介紹(程式碼)的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

相關文章

看更多