主頁 > 知識庫 > PHP7下協(xié)程的實現(xiàn)方法詳解

PHP7下協(xié)程的實現(xiàn)方法詳解

熱門標簽:外呼系統(tǒng)人工客服 百度地圖標注類型是酒店 隨州銷售電銷機器人公司 全國各省地圖標注點 網(wǎng)絡(luò)電話400申請 400電話申請辦理 商丘外呼系統(tǒng)好處 福建高頻外呼防封系統(tǒng)哪家好 周口網(wǎng)絡(luò)回撥外呼系統(tǒng)

前言

相信大家都聽說過『協(xié)程』這個概念吧。

但是有些同學對這個概念似懂非懂,不知道怎么實現(xiàn),怎么用,用在哪,甚至有些人認為yield就是協(xié)程!

我始終相信,如果你無法準確地表達出一個知識點的話,我可以認為你就是不懂。

如果你之前了解過利用PHP實現(xiàn)協(xié)程的話,你肯定看過鳥哥的那篇文章:在PHP中使用協(xié)程實現(xiàn)多任務(wù)調(diào)度| 風雪之隅

鳥哥這篇文章是從國外的作者翻譯來的,翻譯的簡潔明了,也給出了具體的例子了。

我寫這篇文章的目的,是想對鳥哥文章做更加充足的補充,畢竟有部分同學的基礎(chǔ)還是不夠好,看得也是云頭霧里的。

什么是協(xié)程

先搞清楚,什么是協(xié)程。

你可能已經(jīng)聽過『進程』和『線程』這兩個概念。

進程就是二進制可執(zhí)行文件在計算機內(nèi)存里的一個運行實例,就好比你的.exe文件是個類,進程就是new出來的那個實例。

進程是計算機系統(tǒng)進行資源分配和調(diào)度的基本單位(調(diào)度單位這里別糾結(jié)線程進程的),每個CPU下同一時刻只能處理一個進程。

所謂的并行,只不過是看起來并行,CPU事實上在用很快的速度切換不同的進程。

進程的切換需要進行系統(tǒng)調(diào)用,CPU要保存當前進程的各個信息,同時還會使CPUCache被廢掉。

所以進程切換不到費不得已就不做。

那么怎么實現(xiàn)『進程切換不到費不得已就不做』呢?

首先進程被切換的條件是:進程執(zhí)行完畢、分配給進程的CPU時間片結(jié)束,系統(tǒng)發(fā)生中斷需要處理,或者進程等待必要的資源(進程阻塞)等。你想下,前面幾種情況自然沒有什么話可說,但是如果是在阻塞等待,是不是就浪費了。

其實阻塞的話我們的程序還有其他可執(zhí)行的地方可以執(zhí)行,不一定要傻傻的等!

所以就有了線程。

線程簡單理解就是一個『微進程』,專門跑一個函數(shù)(邏輯流)。

所以我們就可以在編寫程序的過程中將可以同時運行的函數(shù)用線程來體現(xiàn)了。

線程有兩種類型,一種是由內(nèi)核來管理和調(diào)度。

我們說,只要涉及需要內(nèi)核參與管理調(diào)度的,代價都是很大的。這種線程其實也就解決了當一個進程中,某個正在執(zhí)行的線程遇到阻塞,我們可以調(diào)度另外一個可運行的線程來跑,但是還是在同一個進程里,所以沒有了進程切換。

還有另外一種線程,他的調(diào)度是由程序員自己寫程序來管理的,對內(nèi)核來說不可見。這種線程叫做『用戶空間線程』。

協(xié)程可以理解就是一種用戶空間線程。

協(xié)程,有幾個特點:

  • 協(xié)同,因為是由程序員自己寫的調(diào)度策略,其通過協(xié)作而不是搶占來進行切換
  • 在用戶態(tài)完成創(chuàng)建,切換和銷毀
  • ⚠️ 從編程角度上看,協(xié)程的思想本質(zhì)上就是控制流的主動讓出(yield)和恢復(resume)機制
  • 迭代器經(jīng)常用來實現(xiàn)協(xié)程

說到這里,你應該明白協(xié)程的基本概念了吧?

PHP實現(xiàn)協(xié)程

一步一步來,從解釋概念說起!

可迭代對象

PHP5提供了一種定義對象的方法使其可以通過單元列表來遍歷,例如用foreach語句。

你如果要實現(xiàn)一個可迭代對象,你就要實現(xiàn)Iterator接口:

?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() {
  $var = next($this->var);
  echo "next: $var\n";
  return $var;
 }
 public function valid() {
  $var = $this->current() !== false;
  echo "valid: {$var}\n";
  return $var;
 }
}
$values = array(1,2,3);
$it = new MyIterator($values);
foreach ($it as $a => $b) {
 print "$a: $b\n";
}

生成器

可以說之前為了擁有一個能夠被foreach遍歷的對象,你不得不去實現(xiàn)一堆的方法,yield關(guān)鍵字就是為了簡化這個過程。

生成器提供了一種更容易的方法來實現(xiàn)簡單的對象迭代,相比較定義類實現(xiàn)Iterator接口的方式,性能開銷和復雜性大大降低。

?php
function xrange($start, $end, $step = 1) {
 for ($i = $start; $i = $end; $i += $step) {
  yield $i;
 }
}
foreach (xrange(1, 1000000) as $num) {
 echo $num, "\n";
}

記住,一個函數(shù)中如果用了yield,他就是一個生成器,直接調(diào)用他是沒有用的,不能等同于一個函數(shù)那樣去執(zhí)行!

所以,yield就是yield,下次誰再說yield是協(xié)程,我肯定把你xxxx。

PHP協(xié)程

前面介紹協(xié)程的時候說了,協(xié)程需要程序員自己去編寫調(diào)度機制,下面我們來看這個機制怎么寫。

0)生成器正確使用

既然生成器不能像函數(shù)一樣直接調(diào)用,那么怎么才能調(diào)用呢?

方法如下:

  • foreach他
  • send($value)
  • current / next...

1)Task實現(xiàn)

Task就是一個任務(wù)的抽象,剛剛我們說了協(xié)程就是用戶空間協(xié)程,線程可以理解就是跑一個函數(shù)。

所以Task的構(gòu)造函數(shù)中就是接收一個閉包函數(shù),我們命名為coroutine。

/**
 * Task任務(wù)類
 */
class Task
{
 protected $taskId;
 protected $coroutine;
 protected $beforeFirstYield = true;
 protected $sendValue;

 /**
  * Task constructor.
  * @param $taskId
  * @param Generator $coroutine
  */
 public function __construct($taskId, Generator $coroutine)
 {
  $this->taskId = $taskId;
  $this->coroutine = $coroutine;
 }
 /**
  * 獲取當前的Task的ID
  * 
  * @return mixed
  */
 public function getTaskId()
 {
  return $this->taskId;
 }
 /**
  * 判斷Task執(zhí)行完畢了沒有
  * 
  * @return bool
  */
 public function isFinished()
 {
  return !$this->coroutine->valid();
 }
 /**
  * 設(shè)置下次要傳給協(xié)程的值,比如 $id = (yield $xxxx),這個值就給了$id了
  * 
  * @param $value
  */
 public function setSendValue($value)
 {
  $this->sendValue = $value;
 }
 /**
  * 運行任務(wù)
  * 
  * @return mixed
  */
 public function run()
 {
  // 這里要注意,生成器的開始會reset,所以第一個值要用current獲取
  if ($this->beforeFirstYield) {
   $this->beforeFirstYield = false;
   return $this->coroutine->current();
  } else {
   // 我們說過了,用send去調(diào)用一個生成器
   $retval = $this->coroutine->send($this->sendValue);
   $this->sendValue = null;
   return $retval;
  }
 }
}

2)Scheduler實現(xiàn)

接下來就是Scheduler這個重點核心部分,他扮演著調(diào)度員的角色。

/**
 * Class Scheduler
 */
Class Scheduler
{
 /**
  * @var SplQueue
  */
 protected $taskQueue;
 /**
  * @var int
  */
 protected $tid = 0;

 /**
  * Scheduler constructor.
  */
 public function __construct()
 {
  /* 原理就是維護了一個隊列,
   * 前面說過,從編程角度上看,協(xié)程的思想本質(zhì)上就是控制流的主動讓出(yield)和恢復(resume)機制
   * */
  $this->taskQueue = new SplQueue();
 }
 /**
  * 增加一個任務(wù)
  *
  * @param Generator $task
  * @return int
  */
 public function addTask(Generator $task)
 {
  $tid = $this->tid;
  $task = new Task($tid, $task);
  $this->taskQueue->enqueue($task);
  $this->tid++;
  return $tid;
 }
 /**
  * 把任務(wù)進入隊列
  *
  * @param Task $task
  */
 public function schedule(Task $task)
 {
  $this->taskQueue->enqueue($task);
 }
 /**
  * 運行調(diào)度器
  */
 public function run()
 {
  while (!$this->taskQueue->isEmpty()) {
   // 任務(wù)出隊
   $task = $this->taskQueue->dequeue();
   $res = $task->run(); // 運行任務(wù)直到 yield

   if (!$task->isFinished()) {
    $this->schedule($task); // 任務(wù)如果還沒完全執(zhí)行完畢,入隊等下次執(zhí)行
   }
  }
 }
}

這樣我們基本就實現(xiàn)了一個協(xié)程調(diào)度器。

你可以使用下面的代碼來測試:

?php
function task1() {
 for ($i = 1; $i = 10; ++$i) {
  echo "This is task 1 iteration $i.\n";
  yield; // 主動讓出CPU的執(zhí)行權(quán)
 }
}
function task2() {
 for ($i = 1; $i = 5; ++$i) {
  echo "This is task 2 iteration $i.\n";
  yield; // 主動讓出CPU的執(zhí)行權(quán)
 }
}
$scheduler = new Scheduler; // 實例化一個調(diào)度器
$scheduler->newTask(task1()); // 添加不同的閉包函數(shù)作為任務(wù)
$scheduler->newTask(task2());
$scheduler->run();

關(guān)鍵說下在哪里能用得到PHP協(xié)程。

function task1() {
  /* 這里有一個遠程任務(wù),需要耗時10s,可能是一個遠程機器抓取分析遠程網(wǎng)址的任務(wù),我們只要提交最后去遠程機器拿結(jié)果就行了 */
  remote_task_commit();
  // 這時候請求發(fā)出后,我們不要在這里等,主動讓出CPU的執(zhí)行權(quán)給task2運行,他不依賴這個結(jié)果
  yield;
  yield (remote_task_receive());
  ...
} 
function task2() {
 for ($i = 1; $i = 5; ++$i) {
  echo "This is task 2 iteration $i.\n";
  yield; // 主動讓出CPU的執(zhí)行權(quán)
 }
}

這樣就提高了程序的執(zhí)行效率。

關(guān)于『系統(tǒng)調(diào)用』的實現(xiàn),鳥哥已經(jīng)講得很明白,我這里不再說明。

3)協(xié)程堆棧

鳥哥文中還有一個協(xié)程堆棧的例子。

我們上面說過了,如果在函數(shù)中使用了yield,就不能當做函數(shù)使用。

所以你在一個協(xié)程函數(shù)中嵌套另外一個協(xié)程函數(shù):

?php
function echoTimes($msg, $max) {
 for ($i = 1; $i = $max; ++$i) {
  echo "$msg iteration $i\n";
  yield;
 }
}
function task() {
 echoTimes('foo', 10); // print foo ten times
 echo "---\n";
 echoTimes('bar', 5); // print bar five times
 yield; // force it to be a coroutine
}
$scheduler = new Scheduler;
$scheduler->newTask(task());
$scheduler->run();

這里的echoTimes是執(zhí)行不了的!所以就需要協(xié)程堆棧。

不過沒關(guān)系,我們改一改我們剛剛的代碼。

把Task中的初始化方法改下,因為我們在運行一個Task的時候,我們要分析出他包含了哪些子協(xié)程,然后將子協(xié)程用一個堆棧保存。(C語言學的好的同學自然能理解這里,不理解的同學我建議去了解下進程的內(nèi)存模型是怎么處理函數(shù)調(diào)用)

 /**
  * Task constructor.
  * @param $taskId
  * @param Generator $coroutine
  */
 public function __construct($taskId, Generator $coroutine)
 {
  $this->taskId = $taskId;
  // $this->coroutine = $coroutine;
  // 換成這個,實際Task->run的就是stackedCoroutine這個函數(shù),不是$coroutine保存的閉包函數(shù)了
  $this->coroutine = stackedCoroutine($coroutine); 
 }

當Task->run()的時候,一個循環(huán)來分析:

/**
 * @param Generator $gen
 */
function stackedCoroutine(Generator $gen)
{
 $stack = new SplStack;
 // 不斷遍歷這個傳進來的生成器
 for (; ;) {
  // $gen可以理解為指向當前運行的協(xié)程閉包函數(shù)(生成器)
  $value = $gen->current(); // 獲取中斷點,也就是yield出來的值
  if ($value instanceof Generator) {
   // 如果是也是一個生成器,這就是子協(xié)程了,把當前運行的協(xié)程入棧保存
   $stack->push($gen);
   $gen = $value; // 把子協(xié)程函數(shù)給gen,繼續(xù)執(zhí)行,注意接下來就是執(zhí)行子協(xié)程的流程了
   continue;
  }
  // 我們對子協(xié)程返回的結(jié)果做了封裝,下面講
  $isReturnValue = $value instanceof CoroutineReturnValue; // 子協(xié)程返回`$value`需要主協(xié)程幫忙處理 
  if (!$gen->valid() || $isReturnValue) {
   if ($stack->isEmpty()) {
    return;
   }
   // 如果是gen已經(jīng)執(zhí)行完畢,或者遇到子協(xié)程需要返回值給主協(xié)程去處理
   $gen = $stack->pop(); //出棧,得到之前入棧保存的主協(xié)程
   $gen->send($isReturnValue ? $value->getValue() : NULL); // 調(diào)用主協(xié)程處理子協(xié)程的輸出值
   continue;
  }
  $gen->send(yield $gen->key() => $value); // 繼續(xù)執(zhí)行子協(xié)程
 }
}

然后我們增加echoTime的結(jié)束標示:

class CoroutineReturnValue {
 protected $value;
 
 public function __construct($value) {
  $this->value = $value;
 }
 // 獲取能把子協(xié)程的輸出值給主協(xié)程,作為主協(xié)程的send參數(shù)
 public function getValue() {
  return $this->value;
 }
}
function retval($value) {
 return new CoroutineReturnValue($value);
}

然后修改echoTimes:

function echoTimes($msg, $max) {
 for ($i = 1; $i = $max; ++$i) {
  echo "$msg iteration $i\n";
  yield;
 }
 yield retval(""); // 增加這個作為結(jié)束標示
}

Task變?yōu)椋?/p>

function task1()
{
 yield echoTimes('bar', 5);
}

這樣就實現(xiàn)了一個協(xié)程堆棧,現(xiàn)在你可以舉一反三了。

4)PHP7中yield from關(guān)鍵字

PHP7中增加了yield from,所以我們不需要自己實現(xiàn)攜程堆棧,真實太好了。

把Task的構(gòu)造函數(shù)改回去:

 public function __construct($taskId, Generator $coroutine)
 {
  $this->taskId = $taskId;
  $this->coroutine = $coroutine;
  // $this->coroutine = stackedCoroutine($coroutine); //不需要自己實現(xiàn)了,改回之前的
 }

echoTimes函數(shù):

function echoTimes($msg, $max) {
 for ($i = 1; $i = $max; ++$i) {
  echo "$msg iteration $i\n";
  yield;
 }
}

task1生成器:

function task1()
{
 yield from echoTimes('bar', 5);
}

這樣,輕松調(diào)用子協(xié)程。

總結(jié)

這下應該明白怎么實現(xiàn)PHP協(xié)程了吧?

好了,以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對腳本之家的支持。

您可能感興趣的文章:
  • php基于協(xié)程實現(xiàn)異步的方法分析
  • 詳解php協(xié)程知識點
  • PHP生成器(generator)和協(xié)程的實現(xiàn)方法詳解
  • 關(guān)于PHP中協(xié)程和阻塞的一些理解與思考
  • PHP 進程池與輪詢調(diào)度算法實現(xiàn)多任務(wù)的示例代碼
  • PHP定時執(zhí)行計劃任務(wù)的多種方法小結(jié)
  • php定時計劃任務(wù)的實現(xiàn)方法詳解
  • php守護進程 加linux命令nohup實現(xiàn)任務(wù)每秒執(zhí)行一次
  • PHP中使用sleep函數(shù)實現(xiàn)定時任務(wù)實例分享
  • PHP實現(xiàn)簡單的協(xié)程任務(wù)調(diào)度demo示例

標簽:六安 十堰 定西 佛山 南寧 迪慶 樂山 海南

巨人網(wǎng)絡(luò)通訊聲明:本文標題《PHP7下協(xié)程的實現(xiàn)方法詳解》,本文關(guān)鍵詞  PHP7,下協(xié),程,的,實現(xiàn),方法,;如發(fā)現(xiàn)本文內(nèi)容存在版權(quán)問題,煩請?zhí)峁┫嚓P(guān)信息告之我們,我們將及時溝通與處理。本站內(nèi)容系統(tǒng)采集于網(wǎng)絡(luò),涉及言論、版權(quán)與本站無關(guān)。
  • 相關(guān)文章
  • 下面列出與本文章《PHP7下協(xié)程的實現(xiàn)方法詳解》相關(guān)的同類信息!
  • 本頁收集關(guān)于PHP7下協(xié)程的實現(xiàn)方法詳解的相關(guān)信息資訊供網(wǎng)民參考!
  • 推薦文章