php yield關鍵字以及協程的實現


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幀

具體更新內容文檔: https://wiki.swoole.com/wiki/page/966.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM