天天看點

什麼是協程_什麼時候使用協程和線程PHP實作協程

大家好,又見面了,我是你們的朋友全棧君。

先搞清楚,什麼是協程。

你可能已經聽過『程序』和『線程』這兩個概念。

程序就是二進制可執行檔案在計算機記憶體裡的一個運作執行個體,就好比你的.exe檔案是個類,程序就是new出來的那個執行個體。

程序是計算機系統進行資源配置設定和排程的基本機關(排程機關這裡别糾結線程程序的),每個CPU下同一時刻隻能處理一個程序。

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

程序的切換需要進行系統調用,CPU要儲存目前程序的各個資訊,同時還會使CPUCache被廢掉。

是以程序切換不到非不得已就不做。

那麼怎麼實作『程序切換不到非不得已就不做』呢?

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

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

是以就有了線程。

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

是以我們就可以在編寫程式的過程中将可以同時運作的函數用線程來展現了。

線程有兩種類型,一種是由核心來管理和排程。

我們說,隻要涉及需要核心參與管理排程的,代價都是很大的。這種線程其實也就解決了當一個程序中,某個正在執行的線程遇到阻塞,我們可以排程另外一個可運作的線程來跑,但是還是在同一個程序裡,是以沒有了程序切換。

還有另外一種線程,他的排程是由程式員自己寫程式來管理的,對核心來說不可見。這種線程叫做『使用者空間線程』。

協程可以了解就是一種使用者空間線程。

協程,有幾個特點:

  • 協同,因為是由程式員自己寫的排程政策,其通過協作而不是搶占來進行切換
  • 在使用者态完成建立,切換和銷毀
  • ⚠️ 從程式設計角度上看,協程的思想本質上就是控制流的主動讓出(yield)和恢複(resume)機制
  • generator經常用來實作協程

說到這裡,你應該明白協程的基本概念了吧?

PHP實作協程

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

可疊代對象

PHP5提供了一種定義對象的方法使其可以通過單元清單來周遊,例如用

foreach

語句。

你如果要實作一個可疊代對象,你就要實作

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

周遊的對象,你不得不去實作一堆的方法,

yield

關鍵字就是為了簡化這個過程。

生成器提供了一種更容易的方法來實作簡單的對象疊代,相比較定義類實作

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"; }           

複制

記住,一個函數中如果用了

yield

,他就是一個生成器,直接調用他是沒有用的,不能等同于一個函數那樣去執行!

是以,

yield

就是

yield

,下次誰再說

yield

是協程,我肯定把你xxxx。

PHP協程

前面介紹協程的時候說了,協程需要程式員自己去編寫排程機制,下面我們來看這個機制怎麼寫。

0)生成器正确使用

既然生成器不能像函數一樣直接調用,那麼怎麼才能調用呢?

方法如下:

  1. foreach他
  2. send($value)
  3. current / next…

1)Task實作

Task就是一個任務的抽象,剛剛我們說了協程就是使用者空間線程,線程可以了解就是跑一個函數。

是以Task的構造函數中就是接收一個閉包函數,我們命名為

coroutine

/** * Task任務類 */ 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執行完畢了沒有 * * @return bool */ public function isFinished() { return !$this->coroutine->valid(); } /** * 設定下次要傳給協程的值,比如 $id = (yield $xxxx),這個值就給了$id了 * * @param $value */ public function setSendValue($value) { $this->sendValue = $value; } /** * 運作任務 * * @return mixed */ public function run() { // 這裡要注意,生成器的開始會reset,是以第一個值要用current擷取 if ($this->beforeFirstYield) { $this->beforeFirstYield = false; return $this->coroutine->current(); } else { // 我們說過了,用send去調用一個生成器 $retval = $this->coroutine->send($this->sendValue); $this->sendValue = null; return $retval; } } }           

複制

2)Scheduler實作

接下來就是

Scheduler

這個重點核心部分,他扮演着排程員的角色。

/** * Class Scheduler */ Class Scheduler { /** * @var SplQueue */ protected $taskQueue; /** * @var int */ protected $tid = 0; /** * Scheduler constructor. */ public function __construct() { /* 原理就是維護了一個隊列, * 前面說過,從程式設計角度上看,協程的思想本質上就是控制流的主動讓出(yield)和恢複(resume)機制 * */ $this->taskQueue = new SplQueue(); } /** * 增加一個任務 * * @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; } /** * 把任務進入隊列 * * @param Task $task */ public function schedule(Task $task) { $this->taskQueue->enqueue($task); } /** * 運作排程器 */ public function run() { while (!$this->taskQueue->isEmpty()) { // 任務出隊 $task = $this->taskQueue->dequeue(); $res = $task->run(); // 運作任務直到 yield if (!$task->isFinished()) { $this->schedule($task); // 任務如果還沒完全執行完畢,入隊等下次執行 } } } }           

複制

這樣我們基本就實作了一個協程排程器。

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

<?php function task1() { for ($i = 1; $i <= 10; ++$i) { echo "This is task 1 iteration $i.\n"; yield; // 主動讓出CPU的執行權 } } function task2() { for ($i = 1; $i <= 5; ++$i) { echo "This is task 2 iteration $i.\n"; yield; // 主動讓出CPU的執行權 } } $scheduler = new Scheduler; // 執行個體化一個排程器 $scheduler->addTask(task1()); // 添加不同的閉包函數作為任務 $scheduler->addTask(task2()); $scheduler->run();           

複制

關鍵說下在哪裡能用得到PHP協程。

function task1() { /* 這裡有一個遠端任務,需要耗時10s,可能是一個遠端機器抓取分析遠端網址的任務,我們隻要送出最後去遠端機器拿結果就行了 */ remote_task_commit(); // 這時候請求發出後,我們不要在這裡等,主動讓出CPU的執行權給task2運作,他不依賴這個結果 yield; yield (remote_task_receive()); ... } function task2() { for ($i = 1; $i <= 5; ++$i) { echo "This is task 2 iteration $i.\n"; yield; // 主動讓出CPU的執行權 } }           

複制

這樣就提高了程式的執行效率。

關于『系統調用』的實作,鳥哥已經講得很明白,我這裡不再說明。

3)協程堆棧

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

我們上面說過了,如果在函數中使用了

yield

,就不能當做函數使用。

是以你在一個協程函數中嵌套另外一個協程函數:

<?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->addTask(task()); $scheduler->run();           

複制

這裡的echoTimes是執行不了的!是以就需要協程堆棧。

不過沒關系,我們改一改我們剛剛的代碼。

把Task中的初始化方法改下,因為我們在運作一個Task的時候,我們要分析出他包含了哪些子協程,然後将子協程用一個堆棧儲存。(C語言學的好的同學自然能了解這裡,不了解的同學我建議去了解下程序的記憶體模型是怎麼處理函數調用)

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

複制

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

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

複制

然後我們增加echoTime的結束标示:

class CoroutineReturnValue { protected $value; public function __construct($value) { $this->value = $value; } // 擷取能把子協程的輸出值給主協程,作為主協程的send參數 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(""); // 增加這個作為結束标示 }           

複制

Task

變為:

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

複制

這樣就實作了一個協程堆棧,現在你可以舉一反三了。

4)PHP7中yield from關鍵字

PHP7中增加了

yield from

,是以我們不需要自己實作攜程堆棧,真是太好了。

把Task的構造函數改回去:

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

複制

echoTimes

函數:

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

複制

task1

生成器:

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

複制

這樣,輕松調用子協程。

釋出者:全棧程式員棧長,轉載請注明出處:https://javaforall.cn/167515.html原文連結:https://javaforall.cn