PHP協程入門詳解

NO IMAGE

概念

咱們知道多程序和多執行緒是實現併發的有效方式。但多程序的上下文切換資源開銷太大;多執行緒開銷相比要小很多,也是現在主流的做法,但其的控制權在核心,從而使使用者(程式設計師)失去了對程式碼的控制,而且執行緒的上下文切換也是有一定開銷的。 這時為了解決以上問題,”協程”(coroutine)的概念就產生了。你可以將協程理解為更輕量級的執行緒。這種執行緒叫做“使用者空間執行緒“。協程,有下面兩個特點:

協同。因為是由程式設計師自己寫的排程策略,其通過協作而不是搶佔來進行切換
在使用者態完成建立,切換和銷燬

PHP對協程的支援是在迭代生成器的基礎上, 增加了可以回送資料給生成器的功能(呼叫者傳送資料給被呼叫的生成器函式)。 這就把生成器到呼叫者的單向通訊轉變為兩者之間的雙向通訊。

迭代器

迭代器的概念這裡就不贅述了。下面看看我們自己實現的一個迭代器。

class MyIterator implements Iterator
{
private $var = array();
public function __construct($array)
{
if (is_array($array)) {
$this->var = $array;
}
}
public function rewind() {   // 第一次迭代時候會執行(或呼叫該方法的時候),後面的迭代將不會執行。
echo "rewinding\n";
reset($this->var);  
}
public function current() {
$var = current($this->var);
echo "current: $var\n";
return $var;
}
public function key() {
$var = key($this->var);
echo "key: $var\n";
return $var;
}
public function next() {    // 最後執行,就是執行完下面sleep(2)後再執行。(執行了next本次迭代才算結束)
$var = next($this->var);
echo "next: $var\n";
return $var;
}
public function valid() {      // 當valid返回false的時候迭代結束
$var = $this->current() !== false;
echo "valid: {$var}\n";
return $var;
}
}
$values = array(1,2,3,4);
$it = new MyIterator($values);
foreach ($it as $a => $b) { // 進行迭代(每次迭代,會依次執行以下方法: rewind(特別之處見上面解釋), valid, current, key, next)
print "=====\n";
sleep(2);
}

輸出:

rewinding
current: 1  // 因為valid裡面呼叫了current, 這裡current出來一次
valid: 1
current: 1
key: 0
=====
next: 2
current: 2
valid: 1
current: 2
key: 1
=====
next: 3
current: 3
valid: 1
current: 3
key: 2
=====
next: 4
current: 4
valid: 1
current: 4
key: 3
=====
next: 
current: 
valid:    // valid返回false,迭代結束

生成器

有了yeild的方法就是一個生成器(生成器實現了Iterator介面,即一個生成器有迭代器的特點)。生成器的實現如下:

function xrange($start, $end, $step = 1) {
for ($i = $start; $i <= $end; $i  = $step) {
echo $i . "\n";
yield;
}
}
// foreach方式
foreach (xrange(1, 10) as $num) {
}
$gene = xrange(1, 10); // gene就是一個生成器物件
// current
$gene->current();  // 列印1
// next
$gene->next();
$gene->current()  // 列印2

輸出:

1
2
3
4
5
6
7
8
9
10
1
2

生成器各方法詳解可看文件: http://php.net/manual/zh/class.generator.php

注意:

生成器不能像函式一樣直接呼叫,呼叫方法如下:

1. foreach他

2. send($value)  

3. current / next…

yield

yield的語法很靈活,我們用下面的例子,讓大家能明白yield語法的使用。

用例1: 讓出cpu執行權

function task1 () {
for ($i = 1; $i <= 10;   $i) {
echo "This is task 1 iteration $i.\n";
yield;// 遇到yield就會主動讓出CPU的執行權;
}
}
$a = task1(); 
$a->current(); // 執行第一次迭代
$a->send(1);  // 喚醒當時讓出CPU執行權的yield

輸出:

This is task 1 iteration 1.
This is task 1 iteration 2.

用例2: yield的返回

// yield返回
function task2 () {
for ($i = 1; $i <= 10;   $i) {
echo "This is task 2 iteration $i.\n";
yield "lm$i";  // 遇到yield就會主動讓出CPU的執行權,for暫停執行, 然後返回"lm"。放在yield後面的值就是返回值
}
}
$a = task2(); 
$res = $a->current();  // 第一次迭代, 遇到yield返回
var_dump($res);  
$res = $a->send(1);  // 喚醒yield, for繼續執行,遇到yield返回。
var_dump($res); 

輸出:

This is task 2 iteration 1.
string(3) "lm1"
This is task 2 iteration 2.
string(3) "lm2"

用例3: yield接收值

function task3 () {
for ($i = 1; $i <= 10;   $i) {
echo "This is task 3 iteration $i.\n";
$getValue = yield;// 遇到yield就會主動讓出CPU的執行權;send後,將send值賦值給getValue
echo $getValue . " ";
}
}
$a = task3(); 
$a->current();
$a->send("aa");  // 喚醒yield,並將"aa"值賦值給$getValue變數

輸出:

This is task 3 iteration 1.
aa This is task 3 iteration 2.  

用例4: yeild接收和返回寫在一起

function task4 () {
for ($i = 1; $i <= 10;   $i) {
echo "This is task 4 iteration $i.\n";
$ret = yield "lm$i";  // yield, 然後返回lm$i; 當send時,將send過來的值賦值給$ret;
echo $ret;
}
}
$a = task4(); 
var_dump($a->current());     // 返回lm1
var_dump($a->send("hhh "));  // 先喚醒yield, 將"hhh "賦值給$ret,再返回lm2
var_dump($a->send("www "));  // 先喚醒yield, 將"www "賦值給$ret,再返回lm3

輸出:

This is task 4 iteration 1.
string(3) "lm1"
hhh This is task 4 iteration 2.
string(3) "lm2"
www This is task 4 iteration 3.
string(3) "lm3"

 

結語:

如果你有看過鳥哥的這篇文章http://www.laruence.com/2015/05/28/3038.html,應該對協程有個深刻的認識。但裡面內容更適合中高階PHP工程師看,而且還得具備一定的作業系統的知識,所以我在此基礎上用更通俗的方式,闡明一下PHP的協程概念。協程很強大的功能但相對比較複雜, 也比較難被理解。個人目前還沒有遇到合適的場景來使用PHP協程,不過我猜測,由於可以在使用者層面實現多併發,所以多用於CLI模式下的web服務開發,比如Golang的goroutine並不是執行緒,而是協程。還有yield有雙向通訊的功能,所以還可以實現非同步服務,但需要自己寫排程器,比如鳥哥這篇部落格裡面的非阻塞IOweb伺服器就是靠協程實現非同步了實現的。 

以上內容如果有錯誤還請留言交流。