PHP中的yield與協程調度器(二十二節上)

這是一個全新的時代,嶄新的科技日新月異,TA就像疾馳而過的復興號一閃而過,而你僅僅因為臨時下車冒了一袋煙就被永遠地甩下!同步阻塞的時代已經遠去,非同步非阻塞的腳步也早已踏離,迎接我們的是全新時代的並發解決方案 — 協程!協程來了!TA看見,TA征服,TA血液里充斥著狼性,TA就是鐵王座的繼承人,TA就是新時代的帝皇…

而對於PHP而言,內置的原生協程的,有的僅僅只有一個叫做yield的關鍵字,但是這個關鍵字返回的數據類型實際上叫做「生成器」,你說TA叫協程是不太嚴格的。在此之前我寫了兩篇關於PHP yield基礎的文章,建議沒看的先看下基礎語法:

由於這裡的概念和使用邏輯可能比較不太容易理解,所以我們還是通過循序漸進的方式來整明白,首先今天這篇文章嘗試利用yield來實現一個簡單的協程調度器,然後下一篇文章基於這個協程調度器實現一個socket伺服器。網上其實關於PHP yield實現協程調度器的資料文章非常少,官方文檔除了基礎語法外毛都沒講,所以我這裡是參考了鳥哥部落格上那篇文章還有有贊那個基於swoole的協程框架,在此基礎之上按照自己的理解進行了一些整理匯總。

前面我們說過,對於yield而言,TA最重要的作用就是「讓當前正在運行的程式讓出CPU」,然後當程式再次佔據CPU的時候接著從上次停止運行的地方繼續運行。下面我們將調度器和協程任務分別抽象成兩個class:

<?php  /*  * @desc : 一個任務的抽象,你可以理解為,一個協程  * */  class Task {      public $i_task_id;      public $g_coroutine;      public $m_send_value;      public $b_is_first_yield = true;      public function __construct( $i_task_id, Generator $g_coroutine ) {          $this->g_coroutine = $g_coroutine;          $this->i_task_id   = $i_task_id;      }      public function set_send_value( $m_send_value ) {          $this->m_send_value = $m_send_value;      }      public function get_task_id() {          return $this->i_task_id;      }      public function run() {          // 如果是第一次執行yield          // 第一個yield的值要用current方法返回          if ( true === $this->b_is_first_yield ) {              $this->b_is_first_yield = false;              return $this->g_coroutine->current();          }          // 只要不是第一次yield,剩下的值都用send雙向通道里獲取到          else {              $m_yield_ret = $this->g_coroutine->send( $this->m_send_value );              $this->m_send_value = null;              return $m_yield_ret;          }      }      // 注意這個方法的內在邏輯是這樣的      // 如果說當前的coroutine是可用的,那麼就表示「還沒有結束」      // 如果說當前的coroutine是不可用的,那麼就表示「已經結束了」      // 所以,前面要取反,加上!      public function is_finish() {          return !$this->g_coroutine->valid();      }  }

其次是一個粗暴的調度器:

<?php  class Scheduler {      public $i_current_task_id  = 0;     // 任務管理器當前最大的任務id      public $a_task_map = array();        // 創建一個新的調度器,就是初始化一個array用來存儲task對象      public function __construct() {          $this->a_task_map = array();      }        public function new_task( Generator $g_coroutine ) {          $i_task_id = $this->i_current_task_id++;          $o_task    = new Task( $i_task_id, $g_coroutine );          $this->a_task_map[ $i_task_id ] = $o_task;          return $i_task_id;      }        public function run() {          while ( count( $this->a_task_map ) > 0 ) {              $o_task = array_shift( $this->a_task_map );              $o_task->run();              if ( $o_task->is_finish() ) {                  unset( $this->a_task_map[ $o_task->get_task_id() ] );              }              else {                  array_push( $this->a_task_map, $o_task );              }          }      }  }

上面的程式碼里,有一個地方我要重點描述一下,就是下面這段:

public function run() {          // 如果是第一次執行yield          // 第一個yield的值要用current方法返回          if ( true === $this->b_is_first_yield ) {              $this->b_is_first_yield = false;              return $this->g_coroutine->current();          }          // 只要不是第一次yield,剩下的值都用send雙向通道里獲取到          else {              $m_yield_ret = $this->g_coroutine->send( $this->m_send_value );              $this->m_send_value = null;              return $m_yield_ret;          }  }

我們先看個小demo片段,你複製粘貼走,然後運行一下:

<?php  function gen() {      yield 'foo1';      yield 'foo2';      yield 'foo3';  }  $gen = gen();  var_dump($gen->send('something'));

怎麼樣,鐵子,和你腦海里預想的結果一致不?如果不一致,嗯,那就是正常水平;如果一致,那TM也是瞎猜的…為啥會出現這個結果呢,這個也沒為啥,其實就是當你第一次對生成器執行send方法的時候會執行一次隱形的$gen->rewind(),然後第一個yield 'foo1'會被忽略而直接執行第二個yield 'foo2',如何獲得第一個yield 'foo1'的值呢?用$gen->current()即可。

結合上面的結論,然後你再看看Task類的run()方法,明白了不?

然後我們簡單使用一下這個攜程調度器:

// 不要想太多  // 你就粗暴認為這就是一個協程  function task1() {      for ($i = 1; $i <= 10; ++$i) {          echo "This is task 1 iteration $i.n";          yield;      }  }  // 不要想太多  // 你就粗暴認為這就是另外一個協程  function task2() {      for ($i = 1; $i <= 5; ++$i) {          echo "This is task 2 iteration $i.n";          yield;      }  }  // 將這兩個協程任務添加到調度器中  // 讓調度器把這兩個協程任務run起來...  $scheduler = new Scheduler();  $scheduler->new_task( task1() );  $scheduler->new_task( task2() );  $scheduler->run();

複製粘貼走跑一下,至於你們那裡是啥樣我不知道,反正我這裡是這樣的:

這就是一個非常粗暴簡單的協程調度器,今天這篇是正式打開一扇門,上面程式碼你們好好琢磨琢磨想想,下篇會結合socket會非常噁心,同時:下一篇也將是《PHP網路編程》的最後一章終章,自此我們就徹底說完了PHP網路編程里出現的同步阻塞伺服器、非同步非阻塞伺服器、協程,至於各位有沒有收穫,已經不在我了,全看諸君自己了。

PHP網路編程寫完了,後面開個什麼短平快的系列呢?C語言?APUE?手寫Redis?接著附近的人?