PHP 協程

朱其鵬發表於2018-11-13

協程

“協程”就是使用者態的執行緒

要理解是什麼是”使用者態的執行緒”,必然就要先理解什麼是”核心態的執行緒”。 核心態的執行緒是由作業系統來進行排程的,在切換執行緒上下文時,要先儲存上一個執行緒的上下文,然後執行下一個執行緒,當條件滿足時,切換回上一個執行緒,並恢復上下文。 協程也是如此,只不過,使用者態的執行緒不是由作業系統來排程的,而是由程式設計師來排程的,是在使用者態的 – 摘自連線描述

關於”使用者態執行緒”,我們用個小例子來加深理解

我們有兩個函式 task1,task2,我們來手動排程它們的執行順序,比如在task1執行一半的時候去執行task2,兩個或者多個函式之間交替執行(這就是協程的概念)。

我們來個正常的函式呼叫方式:

<?php

function task1()
{
    echo "task1函式 執行1\n";
    echo "task1函式 執行2\n";
}

function task2()
{
    echo "task2函式 執行1\n";
    echo "task2函式 執行2\n";
}

// 排程
task1();
task2();

可想而知,以上的輸出肯定是:

task1函式 執行第1
task1函式 執行第2
task2函式 執行第1
task2函式 執行第2

但是我想在程式輸出task1函式 執行1之後就輸出task2函式 執行1怎麼辦?

這個時候 yield 就派上用場了,PHP裡的協程是需要藉助 yield 來完成的。記住,yield 不是協程,而是協程需要藉助 yield 的特性來實現。

<?php

function task1()
{
    echo "task1函式 執行1\n";
    yield;
    echo "task1函式 執行2\n";
}

function task2()
{
    echo "task2函式 執行1\n";
    yield;
    echo "task2函式 執行2\n";
}

// 排程
$task1 = task1();  // 返回一個生成器
$task2 = task2();  // 返回一個生成器
$task1->current();
$task2->current();

以上輸出:

task1函式 執行1
task2函式 執行1

很好,以上結果達到了我們的預期。但是怎麼讓函式里的程式碼往下執行呢?

呼叫生成器的next方法:

$task1->next();
$task2->next();

最後你將看到的輸出結果是兩個函式交替執行輸出的:

task1函式 執行1
task2函式 執行1
task1函式 執行2
task2函式 執行2

小段總結

以上的程式碼實現可以抽象出兩個概念,任務排程任務就是task函式,排程就是我們怎麼去呼叫這些task函式

排程器和任務生成器

上一個小段總結裡有兩個概念叫任務排程,我們簡單的封裝個任務生成器和排程器

// 任務生成器
$createTask = (function () {
    $tasks = [];
    return function ($callback) use (&$tasks) {
        $task = [
            'task' => $callback(),
            'id' => count($tasks) + 1,
        ];
        array_push($tasks, $task);
        return $task;
    };
})();

// 排程器
function schedule($tasks)
{
    $first = [];
    while (!empty($tasks)) {
        $task = array_shift($tasks);
        if (!array_key_exists($task['id'], $first)) {
            $first[$task['id']] = true;
            $task['task']->current();
        } else {
            $task['task']->next();
        }
        if ($task['task']->valid()) {
            array_push($tasks, $task);
        }
    }
}

使用

$tasks = [
    $createTask(function () {
        echo "任務1 執行第1次\n";
        yield;
        echo "任務1 執行第2次\n";
    }),
    $createTask(function () {
        echo "任務2 執行第1次\n";
        yield;
        echo "任務2 執行第2次\n";
    })
];
schedule($tasks);

輸出結果:

任務1 執行第1次
任務2 執行第1次
任務1 執行第2次
任務2 執行第2

可以從結果看出,排程器已經實現了多個任務之間進行協作。

網路請求

現在有個需求!就是任務在遇到網路請求的時候,我們無需等待網路請求的響應結果,而是遇到網路請求的時候,把這個任務掛起,然後去執行其它任務,等網路請求收到響應結果了再通知我們處理

這時候需要我們用到非阻塞IO呼叫相關技術,涉及到系統核心層面,想了解可以點選連結描述

在PHP裡我們需要安裝個擴充套件eio,大家自行安裝

pecl install eio

編碼:

$tasks = [
    $createTask(function () {
        echo "任務1 執行第1次\n";
        yield;
        echo "任務1 執行第2次\n";
    }),
    $createTask(function () {
        echo "任務2 執行第1次\n";
        eio_custom(function () {
            return file_get_contents('https://laravel-china.org');
        }, EIO_PRI_DEFAULT, function ($data, $ret) {
            echo "請求完成\n";
        });
        yield;
        echo "任務2 執行第2次\n";
    })
];

schedule($tasks);
eio_event_loop();

任務2 執行第1次的時候,遇到網路請求,我們把請求任務交給系統核心,然後切換到其它任務去,等請求任務完成後回撥我們傳入的函式。

輸出結果:

任務1 執行第1次
任務2 執行第1次
任務1 執行第2次
任務2 執行第2次
任務2 執行第1次的請求完成

完!

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章