PHP7下的協程實現

前言

相信你們都據說過『協程』這個概念吧。php

可是有些同窗對這個概念似懂非懂,不知道怎麼實現,怎麼用,用在哪,甚至有些人認爲yield就是協程!html

我始終相信,若是你沒法準確地表達出一個知識點的話,我能夠認爲你就是不懂。程序員

若是你以前瞭解過利用PHP實現協程的話,你確定看過鳥哥的那篇文章:在PHP中使用協程實現多任務調度| 風雪之隅 編程

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

我寫這篇文章的目的,是想對鳥哥文章作更加充足的補充,畢竟有部分同窗的基礎仍是不夠好,看得也是雲頭霧裏的。閉包

  1. 歡迎微博關注我 @碼雲 。2. 全互聯網最全的PHP技能圖譜:https://bruceit.com/skills

什麼是協程

先搞清楚,什麼是協程。函數

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

進程就是二進制可執行文件在計算機內存裏的一個運行實例,就比如你的.exe文件是個類,進程就是new出來的那個實例。測試

進程是計算機系統進行資源分配和調度的基本單位(調度單位這裏別糾結線程進程的),每一個CPU下同一時刻只能處理一個進程。this

所謂的並行,只不過是看起來並行,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);
}

這樣,輕鬆調用子協程。

總結

這下應該明白怎麼實現PHP協程了吧?

建議不要使用PHP的Yield來實現協程,推薦使用 swoole,2.0已經支持了協程,並附帶了部分案例。

End...

相關文章
相關標籤/搜索