Реализация корутины под PHP7

PHP

предисловие

Думаю, все слышали о понятии «сопрограмма».

Тем не менее, некоторые студенты, кажется, понимают эту концепцию, но не знают, как ее реализовать, как ее использовать и где ее использовать Некоторые люди даже думают, что yield — это сопрограмма!

Я всегда считаю, что если вы не можете точно выразить точку зрения, я могу предположить, что вы просто не понимаете.

Если вы уже знали об использовании PHP для реализации сопрограмм, вы наверняка видели статью Brother Bird:Использование сопрограмм для реализации планирования многозадачности в PHP |

Статья брата Берда была переведена от иностранного автора, перевод краткий и ясный, а также приведены конкретные примеры.

Цель написания этой статьи - сделать более адекватные дополнения к статье Брата Берда, ведь у некоторых студентов фундамент не достаточно хорош, и они путаются.

Лично я не люблю писать длинные статьи, следите за мной на Weibo@Облако кода, делитесь знаниями с Weibo каждый день. Статья также записана в моем блоге:bruceit.com/p/A4kSfE

Что такое сопрограмма

Сначала выясните, что такое сопрограмма.

Возможно, вы слышали понятия «процесс» и «поток».

Процесс — это работающий экземпляр двоичного исполняемого файла в памяти компьютера, точно так же, как ваш .exe-файл — это класс, а процесс — это экземпляр, который выходит из нового.

Процесс — это базовая единица распределения ресурсов и планирования в компьютерной системе (не беспокойтесь здесь о потоках процессов).Каждый ЦП может одновременно обрабатывать только один процесс.

Так называемый параллелизм — это всего лишь видимость параллелизма.ЦП на самом деле переключается между различными процессами с очень высокой скоростью.

Переключение процессов требует системного вызова, ЦП нужно сохранять различную информацию текущего процесса, и при этом CPUCache будет сбрасываться.

Поэтому, если переключение процесса невозможно, это нужно делать в крайнем случае.

Итак, как реализовать «переключение процесса не выполняется, если это не должно быть сделано»?

Во-первых, условия для переключения процесса: процесс выполняется, квант времени процессора, выделенный процессу, заканчивается, система прерывается и нуждается в обработке, или процесс ожидает необходимых ресурсов (блокировка процесса) и т. д. Если подумать, то в первых нескольких случаях сказать нечего, но если блокируется и ждет, разве это пустая трата времени?

На самом деле, если наша программа заблокирована, есть другие исполняемые места для ее выполнения, поэтому нам не нужно тупо ждать!

Так что треды есть.

Простое понимание потока — это «микропроцесс», который запускает функцию (логический поток).

Таким образом, мы можем использовать потоки для реализации функций, которые могут выполняться одновременно в процессе написания программ.

Существует два типа потоков, один из которых управляется и планируется ядром.

Мы говорим, что до тех пор, пока это связано с необходимостью участия ядра в планировании управления, стоимость очень высока. Этот тип потока на самом деле решает проблему, заключающуюся в том, что когда работающий поток заблокирован в процессе, мы можем запланировать запуск другого работоспособного потока, но он все еще находится в том же процессе, поэтому нет переключения процессов.

Существует еще один тип потока, планированием которого управляет сам программист, невидимый для ядра. Такие потоки называются «потоками пользовательского пространства».

Сопрограмму можно понимать как своего рода поток пользовательского пространства.

Корутины имеют несколько характеристик:

  • Сотрудничество, потому что это политика планирования, написанная самим программистом, которая переключается посредством сотрудничества, а не вытеснения.
  • Создавайте, переключайте и уничтожайте в пользовательском режиме
  • ⚠️ С точки зрения программирования, идея сопрограммы — это, по сути, активный механизм выхода и возобновления потока управления.
  • Генераторы часто используются для реализации сопрограмм.

Говоря об этом, вы должны понимать основные концепции сопрограмм, верно?

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Есть сопрограмма, я обязательно поставлю тебе хххх.

сопрограммы PHP

Когда я ранее представил сопрограммы, я сказал, что сопрограммы требуют, чтобы программисты сами написали механизм планирования. Давайте посмотрим, как написать этот механизм.

0) Генератор используется правильно

Поскольку генераторы нельзя вызывать напрямую, как функции, как их можно вызывать?

Методы, как показано ниже:

  1. навести его
  2. send($value)
  3. current / next...

1) Реализация задачи

Задача — это абстракция задачи.Мы только что сказали, что сопрограмма — это сопрограмма пользовательского пространства, а поток можно понимать как выполнение функции.

Итак, конструктор 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Основная часть этого фокуса, он играет роль диспетчера.

/**
 * 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的执行权
    }
}

Это повышает эффективность выполнения программы.

Относительно реализации «системного вызова» Brother Bird очень ясно дал понять, и я не буду объяснять это здесь.

3) Стек сопрограмм

В Brother Bird также есть пример стека сопрограмм.

Как мы сказали выше, если используется функция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

Добавлено в PHP7yield from, так что нам не нужно самим реализовывать стек Ctrip, и это здорово.

Измените конструктор 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, верно?

End...