PHP協程(1):簡略

基本概念

援引幾個博客上的話:

對於單核處理器,多進程實現多任務的原理是讓操作系統給一個任務每次分配一定的 CPU時間片,然後中斷、讓下一個任務執行一定的時間片接着再中斷並繼續執行下一個,如此反覆。由於切換執行任務的速度非常快,給外部用戶的感受就是多個任務的執行是同時進行的。多進程的調度是由操作系統來實現的,進程自身不能控制自己何時被調度,也就是說:
進程的調度是由外層調度器搶佔式實現的
而協程要求當前正在運行的任務自動把控制權回傳給調度器,這樣就可以繼續運行其他任務。這與『搶佔式』的多任務正好相反,搶佔多任務的調度器可以強制中斷正在運行的任務, 不管它自己有沒有意願。『協作式多任務』在 Windows 的早期版本 (windows95)和 Mac OS 中有使用,不過它們後來都切換到『搶佔式多任務』了。理由相當明確:如果僅依靠程序自動交出控制的話,那麼一些惡意程序將會很容易佔用全部 CPU時間而不與其他任務共享。
協程的調度是由協程自身主動讓出控制權到外層調度器實現的
協程可以理解爲純用戶態的線程,通過協作而不是搶佔來進行任務切換。相對於進程或者線程,協程所有的操作都可以在用戶態而非操作系統內核態完成,創建和切換的消耗非常低。簡單的說 Coroutine(協程) 就是提供一種方法來中斷當前任務的執行,保存當前的局部變量,下次再過來又可以恢復當前局部變量繼續執行。
轉載來源:http://www.jianshu.com/p/edef1cb7fee6

然後看下韓天峯對於應用協程的看法:

當程序員還沉浸在解決C10K問題帶來的成就感時,一個新的問題被拋出了。異步嵌套回調太TM難寫了。尤其是Node.js層層回調,縮進了幾十層,要把程序員逼瘋了。於是一個新的技術被提出來了,那就是協程(coroutine)。這個技術本質上也是異步非阻塞技術,它是將事件回調進行了包裝,讓程序員看不到裏面的事件循環。程序員就像寫阻塞代碼一樣簡單。比如調用 client->recv() 等待接收數據時,就像阻塞代碼一樣寫。實際上是底層庫在執行recv時悄悄保存了一個狀態,比如代碼行數,局部變量的值。然後就跳回到EventLoop中了。什麼時候真的數據到來時,它再把剛纔保存的代碼行數,局部變量值取出來,又開始繼續執行。

這個就像時間禁止的遊戲一樣,國王對巫師說“我必須馬上得到寶物,不然就砍了你的腦袋”,巫師唸了一句時間停止的咒語,直到過了1年後勇士們才把寶物送來。這時候巫師解開咒語,把寶物交給國王。這裏國王就可以理解成協程,他根本沒感覺到時間停止,在他停止到醒來期間發生了什麼他不知道,也不關心。

這就是協程的本質。協程是異步非阻塞的另外一種展現形式。Golang,Erlang,Lua協程都是這個模型。
轉載地址:http://rango.swoole.com/archives/381

這裏單單看這一段可能無法理解,建議去看下這篇轉載地址的全文;這裏其實說的就是協程對於異步的應用場景;

PHP的協程

PHP實現協程,簡單點說是通過yield關鍵字,準確點說是通過生成器generator類來實現的;
看PHP裏面通過generator實現的釋放控制:

例子1

function task()
{
    echo "生成器 內 第一代碼段" . PHP_EOL;
    yield;
    echo "生成器 內 第二代碼段" . PHP_EOL;
    yield;
    echo "生成器 內 第三代碼段" . PHP_EOL;
}

$task = task(); // 初始化生成器
$task->current(); // 執行到第一個yield
$task->next(); // 執行到第二個yield
$task->next(); // 執行第二個yield之後的代碼段
/**
 * 輸出:
 * 生成器 內 第一代碼段
 * 生成器 內 第二代碼段
 * 生成器 內 第三代碼段
 */

這個非常就簡單的例子裏面,task函數裏面的代碼的執行是可控的,什麼時候中斷,什麼時候執行,是由生成器控制的;而這個中斷,就是釋放控制權,代碼可以繼續執行下去;
Generator生成器可以參考http://blog.csdn.net/alexander_phper/article/details/78523876
再看一個例子:

例子2

function task()
{
    // 這裏執行的代碼稱爲生成器內
    echo "生成器 內 第一代碼段" . PHP_EOL;
    yield;
    echo "生成器 內 第二代碼段" . PHP_EOL;
    yield;
    echo "生成器 內 第三代碼段" . PHP_EOL;
}
// 這裏執行的代碼稱爲生成器外
$task = task(); // 初始化生成器
echo "生成器 外 第一代碼段" . PHP_EOL;
$task->current();
echo "生成器 外 第二代碼段" . PHP_EOL;
$task->next();
echo "生成器 外 第三代碼段" . PHP_EOL;
$task->next();
echo "生成器 外 第四代碼段" . PHP_EOL;
/**
 * 輸出:
 * 生成器 外 第一代碼段
 * 生成器 內 第一代碼段
 * 生成器 外 第二代碼段
 * 生成器 內 第二代碼段
 * 生成器 外 第三代碼段
 * 生成器 內 第三代碼段
 * 生成器 外 第四代碼段
 */

生成器內外的代碼在交替執行着;當運行到生成器裏面的yield關鍵字的時候,程序會釋放控制權,執行別的程序;

PHP協程實現多任務交替執行

生成器可以中斷執行,將控制權交出去,那我們可以讓程序在多個生成器中間切換;

例子3

function task1()
{
    echo "生成器1 內 第一代碼段" . PHP_EOL;
    yield;
    echo "生成器1 內 第二代碼段" . PHP_EOL;
    yield;
    echo "生成器1 內 第三代碼段" . PHP_EOL;
}

function task2()
{
    echo "生成器2 內 第一代碼段" . PHP_EOL;
    yield;
    echo "生成器2 內 第二代碼段" . PHP_EOL;
    yield;
    echo "生成器2 內 第三代碼段" . PHP_EOL;
}

$task1 = task1(); // 初始化生成器
$task2 = task2();
$task1->current();
$task2->current();
$task1->next();
$task2->next();
$task1->next();
$task2->next();
/**
 * 輸出:
 * 生成器1 內 第一代碼段
 * 生成器2 內 第一代碼段
 * 生成器1 內 第二代碼段
 * 生成器2 內 第二代碼段
 * 生成器1 內 第三代碼段
 * 生成器2 內 第三代碼段
 */

現在看起來我們可以在兩個函數中切換着執行(其實是兩個生成器中切換着執行);
這種切換任務的實現方法非常低級,需要手動執行生成器;我們看那一篇原文中的實現方式;
(轉載地址:http://nikic.github.io/2012/12/22/Cooperative-multitasking-using-coroutines-in-PHP.html
(翻譯版地址:http://www.laruence.com/2015/05/28/3038.html);

class Scheduler {
    protected $maxTaskId = 0;
    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;
    }

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

class Task {
    protected $taskId;
    protected $coroutine;
    protected $sendValue = null;
    protected $beforeFirstYield = true;

    public function __construct($taskId, Generator $coroutine) {
        $this->taskId = $taskId;
        $this->coroutine = $coroutine;
    }

    public function getTaskId() {
        return $this->taskId;
    }

    public function setSendValue($sendValue) {
        $this->sendValue = $sendValue;
    }

    public function run() {
        if ($this->beforeFirstYield) {
            $this->beforeFirstYield = false;
            return $this->coroutine->current();
        } else {
            $retval = $this->coroutine->send($this->sendValue);
            $this->sendValue = null;
            return $retval;
        }
    }

    public function isFinished() {
        return !$this->coroutine->valid();
    }
}



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

$scheduler = new Scheduler;

$scheduler->newTask(task1());
$scheduler->newTask(task2());

$scheduler->run();

/**
 * 輸出:
 * This is task 1 iteration 1.
 * This is task 2 iteration 1.
 * This is task 1 iteration 2.
 * This is task 2 iteration 2.
 * This is task 1 iteration 3.
 * This is task 2 iteration 3.
 * This is task 1 iteration 4.
 * This is task 2 iteration 4.
 * This is task 1 iteration 5.
 * This is task 2 iteration 5.
 * This is task 1 iteration 6.
 * This is task 1 iteration 7.
 * This is task 1 iteration 8.
 * This is task 1 iteration 9.
 * This is task 1 iteration 10.
 */

這裏面引入的調度器類Scheduler和任務類Task(一個任務類可以簡單理解就是一個生成器generator),調度器依次控制所有任務執行一次,任務執行一次就是運行到generator的一個yield;所以調度器可以依次執行task1和task2中的代碼;當task2中的代碼運行完畢之後,繼續運行task1中的代碼,直到task1中的代碼也運行完畢;

現在應該有一個疑問,協程可以幹嘛呢?
上面協程那一篇原文中有socket非阻塞IO的應用,可查看鳥哥的翻譯版;
不過協程配合異步是個非常好的應用場景;可以查看韓天峯上面的說法;本人也是經過swoole的異步才接觸到協程這個概念,現在已經用了很久,但是對協程卻很模糊,這纔有了這一篇文章;下一篇讓我們看下通過攜程堆棧實現異步回調,把異步的callback寫法直接變成同步的寫法;

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章