php的yield是在php5.5版本就出來了,而在初級php界卻很少有人提起,我就說說個人對php yield的理解
Iterator接口
在php中,除了數組,對象可以被foreach遍歷之外,還有另外一種特殊對象,也就是繼承了iterator接口的對象,也可以被對象遍歷,但和普通對象的遍歷又有所不同,下面是3種類型的遍歷情況:



可以看出,迭代器的遍歷,會依次調用重置,檢查當前數據,返回當前指針數據,指針下移方法,結束遍歷的條件在於檢查數據返回true或者false
生成器
生成器和迭代器類似,但也完全不同
生成器允許你在 foreach 代碼塊中寫代碼來迭代一組數據而不需要在內存中創建一個數組, 那會使你的內存達到上限,或者會占據可觀的處理時間。相反,你可以寫一個生成器函數,就像一個普通的自定義函數一樣, 和普通函數只返回一次不同的是, 生成器可以根據需要 yield 多次,以便生成需要迭代的值。
生成器使用yield關鍵字進行生成迭代的值
例如:

一:生成器方法
生成器它的內部實現了以下方法:
Generator implements Iterator { //返回當前產生的值 public mixed current ( void ) //返回當前產生的鍵 public mixed key ( void ) //生成器繼續執行 public void next ( void ) //重置迭代器,如果迭代已經開始了,這里會拋出一個異常。 public void rewind ( void ) //向生成器中傳入一個值,當前yield接收值,然后繼續執行下一個yield public mixed send ( mixed $value ) //向生成器中拋入一個異常 public void throw ( Exception $exception ) //檢查迭代器是否被關閉,已被關閉返回 FALSE,否則返回 TRUE public bool valid ( void ) //序列化回調 public void __wakeup ( void ) //返回generator函數的返回值,PHP version 7+ public mixed getReturn ( void ) }
二:語法
生成器的語法有很多種用法,需要一一說明,首先,yield必須有函數包裹,包裹yield的函數稱為"生成器函數",該函數將返回一個可遍歷的對象
1:顛覆常識的yield

可能你在這發現了幾個東西,和之前php完全不同的認知,如果你沒發現,額,那我提出來吧
1:在調用函數返回的時候,可以發現for里面的語句並沒有執行
2:在遍歷一次的時候,可以發現調用函數,卻沒有正常的for循環3次,只循環了一次
3:在遍歷一次的情況時,"存在感2"竟然沒有調用,在一直遍歷的情況下才調用
再看看另一個例子:



什么????while(ture)竟然還能正常的執行下去???沒錯,生成器函數就是這樣的,根據這個例子,我們發現了這些東西:
1:while(true)沒有阻塞調用函數下面的代碼執行,卻導致了下面的echo "額額額"和return 無法執行
2:return 返回值竟然是沒有作用的
3:send(1)時,沒有echo "哈哈",send(2)時,才開始出現"哈哈",
2:yield的其他語法
yield表達式中,也可以賦值,但賦值需要使用括號包裹:

只需要在表達式后面加上$key=>$value,即可生成鍵值的數據:

在函數前增加引用定義,就可以像returning references from functions(從函數返回一個引用)一樣 引用生成值

三:特性總結
1:yield是生成器所需要的關鍵字,必須在函數內部,有yield的函數叫做"生成器函數"
2:調用生成器函數時,函數將返回一個繼承了Iterator的生成器
3:yield作為表達式使用時,可將一個值加入到生成器中進行遍歷,遍歷完會中斷下面的語句運行,並且保存狀態,當下次遍歷時會繼續執行(這就是while(true)沒有造成阻塞的原因)
4:當send傳入參數時,yield可作為一個變量使用,這個變量等於傳入的參數
協程
一:實現個簡單的協程
協程,是一種編程邏輯的轉變,使多個任務能交替運行,而不是之前的一直根據流程往下走,舉個例子
當有一個邏輯,每次調用這個文件時,該文件要做3件事:
1:寫入300個文件
2:發送郵件給500個會員
3:插入100條數據
代碼:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
<?php
function
task1(){
for
(
$i
=0;
$i
<=300;
$i
++){
//寫入文件,大概要3000微秒
usleep(3000);
echo
"寫入文件{$i}\n"
;
}
}
function
task2(){
for
(
$i
=0;
$i
<=500;
$i
++){
//發送郵件給500名會員,大概3000微秒
usleep(3000);
echo
"發送郵件{$i}\n"
;
}
}
function
task3(){
for
(
$i
=0;
$i
<=100;
$i
++){
//模擬插入100條數據,大概3000微秒
usleep(3000);
echo
"插入數據{$i}\n"
;
}
}
task1();
task2();
task3();
|
這樣,就實現了這3個功能了,然而,技術組長又說:
能不能改成交替運行呢?
就是說,寫入文件一次之后,馬上去發送一次郵件,然后再去插入一條數據
然后我改一改:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
<?php
function
task1(
$i
)
{
//使用$i標識 寫入文件,大概要3000微秒
if
(
$i
> 300) {
return
false;
//超過300不用寫了
}
echo
"寫入文件{$i}\n"
;
usleep(3000);
return
true;
}
function
task2(
$i
)
{
//使用$i標識 發送郵件,大概要3000微秒
if
(
$i
> 500) {
return
false;
//超過500不用發送了
}
echo
"發送郵件{$i}\n"
;
usleep(3000);
return
true;
}
function
task3(
$i
)
{
//使用$i標識 插入數據,大概要3000微秒
if
(
$i
> 100) {
return
false;
//超過100不用插入
}
echo
"插入數據{$i}\n"
;
usleep(3000);
return
true;
}
$i
= 0;
$task1Result
= true;
$task2Result
= true;
$task3Result
= true;
while
(true) {
$task1Result
&&
$task1Result
= task1(
$i
);
$task2Result
&&
$task2Result
= task2(
$i
);
$task3Result
&&
$task3Result
= task3(
$i
);
if
(
$task1Result
===false&&
$task2Result
===false&&
$task3Result
===false){
break
;
//全部任務完成,退出循環
}
$i
++;
}
|
運行一下:
代碼1:

代碼2:

確實是實現了任務交替執行,但是代碼2明顯讓代碼變的非常的難讀,擴展性也很差,那么,有沒有更好的方式實現這個功能呢?
這時候我們就必須借助yield了
首先,我們得封裝一個任務類:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
|
/**
* 任務對象
* Class Task
*/
class
Task {
protected
$taskId
;
//任務id
protected
$coroutine
;
//生成器
protected
$sendValue
= null;
//生成器send值
protected
$beforeFirstYield
= true;
//迭代指針是否是第一個
public
function
__construct(
$taskId
, Generator
$coroutine
) {
$this
->taskId =
$taskId
;
$this
->coroutine =
$coroutine
;
}
public
function
getTaskId() {
return
$this
->taskId;
}
/**
* 設置插入數據
* @param $sendValue
*/
public
function
setSendValue(
$sendValue
) {
$this
->sendValue =
$sendValue
;
}
/**
* send數據進行迭代
* @return mixed
*/
public
function
run() {
//如果是
if
(
$this
->beforeFirstYield) {
$this
->beforeFirstYield = false;
var_dump(
$this
->coroutine->current());
return
$this
->coroutine->current();
}
else
{
$retval
=
$this
->coroutine->send(
$this
->sendValue);
$this
->sendValue = null;
return
$retval
;
}
}
/**
* 是否完成
* @return bool
*/
public
function
isFinished() {
return
!
$this
->coroutine->valid();
}
}
|
這個封裝類,可以更好的去調用運行生成器函數,但只有這個也是不夠的,我們還需要一個調度任務類,來代替前面的while:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
/**
* 任務調度
* Class Scheduler
*/
class
Scheduler {
protected
$maxTaskId
= 0;
//任務id
protected
$taskMap
= [];
// taskId => task
protected
$taskQueue
;
//任務隊列
public
function
__construct() {
$this
->taskQueue =
new
SplQueue();
}
public
function
newTask(Generator
$coroutine
) {
$tid
= ++
$this
->maxTaskId;
//新增任務
$task
=
new
Task(
$tid
,
$coroutine
);
$this
->taskMap[
$tid
] =
$task
;
$this
->schedule(
$task
);
return
$tid
;
}
/**
* 任務入列
* @param Task $task
*/
public
function
schedule(Task
$task
) {
$this
->taskQueue->enqueue(
$task
);
}
public
function
run() {
while
(!
$this
->taskQueue->isEmpty()) {
//任務出列進行遍歷生成器數據
$task
=
$this
->taskQueue->dequeue();
$task
->run();
if
(
$task
->isFinished()) {
//完成則刪除該任務
unset(
$this
->taskMap[
$task
->getTaskId()]);
}
else
{
//繼續入列
$this
->schedule(
$task
);
}
}
}
}
|
很好,我們已經有了一個調度類,還有了一個任務類,可以繼續實現上面的功能了:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
function
task1()
{
for
(
$i
= 0;
$i
<= 300;
$i
++) {
//寫入文件,大概要3000微秒
usleep(3000);
echo
"寫入文件{$i}\n"
;
yield
$i
;
}
}
function
task2()
{
for
(
$i
= 0;
$i
<= 500;
$i
++) {
//發送郵件給500名會員,大概3000微秒
usleep(3000);
echo
"發送郵件{$i}\n"
;
yield
$i
;
}
}
function
task3()
{
for
(
$i
= 0;
$i
<= 100;
$i
++) {
//模擬插入100條數據,大概3000微秒
usleep(3000);
echo
"插入數據{$i}\n"
;
yield
$i
;
}
}
$scheduler
=
new
Scheduler;
$scheduler
->newTask(task1());
$scheduler
->newTask(task2());
$scheduler
->newTask(task3());
$scheduler
->run();
|
除了上面的2個類,task函數和代碼1不同的地方,就是多了個yield,那我們試着運行一下:

很好,我們已經實現了可以調度任務,進行任務交叉運行的功能了,這就是"協程"
協程可以將多個不同的任務交叉運行
二:協程與調度器的通信
我們在上面已經實現了一個協程封裝了,但是任務和調度器缺少了通信,我們可以重新封裝下,使協程當中能夠獲取當前的任務id,新增任務,以及殺死任務
先封裝一下調用的封裝:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
class
YieldCall
{
protected
$callback
;
public
function
__construct(callable
$callback
)
{
$this
->callback =
$callback
;
}
/**
* 調用時將返回結果
* @param Task $task
* @param Scheduler $scheduler
* @return mixed
*/
public
function
__invoke(Task
$task
, Scheduler
$scheduler
)
{
$callback
=
$this
->callback;
return
$callback
(
$task
,
$scheduler
);
}
}
|
同時我們需要小小的改動下調度器的run方法:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
public
function
run()
{
while
(!
$this
->taskQueue->isEmpty()) {
$task
=
$this
->taskQueue->dequeue();
$retval
=
$task
->run();
//如果返回的是YieldCall實例,則先執行
if
(
$retval
instanceof
YieldCall) {
$retval
(
$task
,
$this
);
continue
;
}
if
(
$task
->isFinished()) {
unset(
$this
->taskMap[
$task
->getTaskId()]);
}
else
{
$this
->schedule(
$task
);
}
}
}
|
新增 getTaskId函數去返回task_id:
|
1
2
3
4
5
6
7
8
9
10
11
|
function
getTaskId()
{
//返回一個YieldCall的實例
return
new
YieldCall(
//該匿名函數會先獲取任務id,然后send給生成器,並且由YieldCall將task_id返回給生成器函數
function
(Task
$task
, Scheduler
$scheduler
) {
$task
->setSendValue(
$task
->getTaskId());
$scheduler
->schedule(
$task
);
}
);
}
|
然后,我們再修改下task1,task2,task3函數:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
function
task1()
{
$task_id
= (yield getTaskId());
for
(
$i
= 0;
$i
<= 300;
$i
++) {
//寫入文件,大概要3000微秒
usleep(3000);
echo
"任務{$task_id}寫入文件{$i}\n"
;
yield
$i
;
}
}
function
task2()
{
$task_id
= (yield getTaskId());
for
(
$i
= 0;
$i
<= 500;
$i
++) {
//發送郵件給500名會員,大概3000微秒
usleep(3000);
echo
"任務{$task_id}發送郵件{$i}\n"
;
yield
$i
;
}
}
function
task3()
{
$task_id
= (yield getTaskId());
for
(
$i
= 0;
$i
<= 100;
$i
++) {
//模擬插入100條數據,大概3000微秒
usleep(3000);
echo
"任務{$task_id}插入數據{$i}\n"
;
yield
$i
;
}
}
$scheduler
=
new
Scheduler;
$scheduler
->newTask(task1());
$scheduler
->newTask(task2());
$scheduler
->newTask(task3());
$scheduler
->run();
|
執行結果:

這樣的話,當第一次執行的時候,會先調用getTaskId將task_id返回,然后將任務繼續執行,這樣,我們就獲取到了調度器分配給任務的task_id,是不是很神奇?
三:生成新任務以及殺死任務
現在新增了一個需求:當發送郵件給會員時,需要新增一個發送短信的子任務,當會員id大於200時則停止 (別問我為什么要這樣做,我自己都不知道)
同時,我們可以利用YieldCall,去新增任務和殺死任務:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
/**
* 傳入一個生成器函數用於新增任務給調度器調用
* @param Generator $coroutine
* @return YieldCall
*/
function
newTask(Generator
$coroutine
) {
return
new
YieldCall(
//該匿名函數,會在調度器中新增一個任務
function
(Task
$task
, Scheduler
$scheduler
)
use
(
$coroutine
) {
$task
->setSendValue(
$scheduler
->newTask(
$coroutine
));
$scheduler
->schedule(
$task
);
}
);
}
/**
* 殺死一個任務
* @param $tid
* @return YieldCall
*/
function
killTask(
$taskId
) {
return
new
YieldCall(
//該匿名函數,傳入一個任務id,然后讓調度器去殺死該任務
function
(Task
$task
, Scheduler
$scheduler
)
use
(
$taskId
) {
$task
->setSendValue(
$scheduler
->killTask(
$taskId
));
$scheduler
->schedule(
$task
);
}
);
}
|
同時,調度器也得有killTask的方法:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
/**
* 殺死一個任務
* @param $taskId
* @return bool
*/
public
function
killTask(
$taskId
)
{
if
(!isset(
$this
->taskMap[
$taskId
])) {
return
false;
}
unset(
$this
->taskMap[
$taskId
]);
/**
* 遍歷隊列,找出id相同的則刪除
*/
foreach
(
$this
->taskQueue
as
$i
=>
$task
) {
if
(
$task
->getTaskId() ===
$taskId
) {
unset(
$this
->taskQueue[
$i
]);
break
;
}
}
return
true;
}
|
有了新增和刪除,我們就可以重新寫一下task2以及新增task4:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
function
task4(){
$task_id
= (yield getTaskId());
while
(true) {
echo
"任務{$task_id}發送短信\n"
;
yield;
}
}
function
task2()
{
$task_id
= (yield getTaskId());
$child_task_id
= (yield newTask(task4()));
for
(
$i
= 0;
$i
<= 500;
$i
++) {
//發送郵件給500名會員,大概3000微秒
usleep(3000);
echo
"任務{$task_id}發送郵件{$i}\n"
;
yield
$i
;
if
(
$i
==200){
yield killTask(
$child_task_id
);
}
}
}
|
運行結果:

這樣我們就完美的實現了新增任務,以及殺死任務了
總結
前面所說的,協程只是一種編程邏輯,一種寫代碼的技巧,協程能夠幫助我們更好的切換代碼中任務
從上面的例子不難發現,其實協程實現封裝較為麻煩,並且不用協程也能實現這些功能,那為什么要用協程呢?
因為協程可以讓代碼更加的簡潔,任務相互之間獨立區分開,可以使代碼更加的清爽
協程讓我們可以更好的控制切換任務流
前面介紹了那么多,或許有很多人感覺不對,會說"協程不能提升效率嗎?","協程到底用來干什么的?"
或許由上面的例子很難看出協程的用處,那我們繼續舉例子吧:
js ajax是phper都了解的一個技術,
當點擊一個按鈕時,先將點擊事件ajax傳輸給后端進行增加一條點擊數據,然后出現一個動畫,這是一個很正常的事,那么請問,如果ajax是同步,並且在網絡不好的情況,會發生什么呢?
沒錯,點擊之后,頁面將會卡幾秒(網絡不好),請求完畢之后,才會出現一個動畫.
協程的用處就在這了,我們可以利用協程,把一些同步io等待的代碼邏輯,改為異步,在等待的時間內,可以讓cpu去處理其他任務,
就如同小學時候做的一道題:
小明燒開水需要10分鍾,刷牙需要3分鍾,吃早餐需要5分鍾,請問做完這些事情總共需要多少分鍾?
答案是10分鍾,因為在燒開水這個步驟時,不需要坐在那里看水壺燒(異步,io耗時)可以先去刷牙,然后去吃早餐
以上就是php yield關於協程的全部內容了
swoole
由總結可以看出,協程用在最多的應用場景,在於需要io耗時,cpu可以節省出來的場景,並且必須要是異步操作
這里推薦swoole擴展https://www.swoole.com/,
Swoole-4.1.0正式版發布, 主要改動及新特性:
+ PHP原生Redis、PDO、MySQLi輕松協程化, 使用Swoole\Runtime::enableCorotuine()即可將普通的同步阻塞Redis、PDO、MySQLi操作變為協程調度的異步非阻塞IO
+ 協程跟蹤功能: 新增兩個方法 Coroutine::listCoroutines()可遍歷當前所有協程, Coroutine::getBackTrace($cid)可獲取某個協程的函數調用棧
+ 支持在協程和Server中使用exit, 此時將會拋出可捕獲的\Swoole\ExitException異常
+ 移除所有迭代器(table/connection/coroutine_list)的PCRE依賴限制
+ 新增http_compression配置項, 底層會自動判斷客戶端傳入的Accept-Encoding選擇合適的壓縮方法, 支持GoogleBrotli壓縮
+ 重構了底層Channel和協程Http客戶端的C代碼為C++協程模式, 解決歷史遺留的異步時序問題, 穩定性大大提升
+ 更完整穩定的HTTP2支持和SSL處理
+ 增加open_websocket_close_frame配置, 可以在onMessage事件中接收close幀
