PHP-基于yield的内存优化与协程的实现

1.前言

PHP5.5版本之后提供了Generator(生成器)的特性,它通过使用yield来完成简单的对象迭代功能,yield的功能很强大,除了可以优化内存以外,还能实现PHP版的协程支持。

2.Generator(生成器)的介绍

final class Generator implements Iterator {
        /**
         * Throws an exception if the generator is currently after the first yield.
         * @return void
         */
        function rewind() {}
        /**
         * Returns false if the generator has been closed, true otherwise.
         * @return bool
         */
        function valid() {}
        /**
         * Returns whatever was passed to yield or null if nothing was passed or the generator is already closed.
         * @return mixed
         */
        function current() {}
        /**
         * Returns the yielded key or, if none was specified, an auto-incrementing key or null if the generator is already closed.
         * @return mixed
         */
        function key() {}
        /**
         * Resumes the generator (unless the generator is already closed).
         * @return void
         */
        function next() {}

        /**
         * Sets the return value of the yield expression and resumes the generator (unless the generator is already closed).
         * @param mixed $value
         * @return mixed
         */
        function send($value) {}

        /**
         * Throws an exception at the current suspension point in the generator.
         * @param Throwable $exception
         * @return mixed
         */
        function PS_UNRESERVE_PREFIX_throw(Throwable $exception) {}

        /**
         * Returns whatever was passed to return or null if nothing.
         * Throws an exception if the generator is still valid.
         * @link https://wiki.php.net/rfc/generator-return-expressions
         * @return mixed|null
         */
        function getReturn() {}

        /**
         * Serialize callback
         * Throws an exception as generators can't be serialized.
         * @link https://php.net/manual/en/generator.wakeup.php
         * @return void
         */
        public function __wakeup(){}
    }

因为它实现了Iterator(迭代器)的接口,所以可以通过foreach等循环操作来访问yield的返回值。它也是中断函数,遇到第一条yield指令就会中断返回,PHP的协程就是利用了这一特性实现的。

2.yield的介绍

yield支持三种特性:

1.当它作为声明时,它是发送方,用于数据的生成。

2.当作为表达式的时候,它是接收方,用于数据的接收处理。

3.当它用()把声明括起来时,那么它既是发送方也是接收方。

2.1yield作为发送方的使用

yield作为发送方,只要作为声明就可以提供数据的生成。和需要申请大内存的数组方法range(0,10000)不同的是,它不会一次性的计算数组的大小并分配内存,而是先申请一部分内存,然后通过foreach遍历的时候,记录生成器的状态迭代的复用这一部分内存,从而大大的提高了内存的使用效率。Laravel6.0新功能之一,Collection生成大数组用的就是yield优化。

function xrange($start, $limit, $step = 1)
{
    if ($start < $limit) {
        if ($step <= 0) {
            throw new LogicException('Step must be +ve');
        }

        for ($i = $start; $i <= $limit; $i += $step) {
            yield $i;
        }
    } else {
        if ($step >= 0) {
            throw new LogicException('Step must be -ve');
        }

        for ($i = $start; $i >= $limit; $i += $step) {
            yield $i;
        }
    }
}

$start = memory_get_usage(false);
$range = xrange(0, 10000);
printf('xrange: 使用的内存字节数:%d ' . PHP_EOL, memory_get_usage(false) - $start);
unset($range);
$range = range(0,10000);
printf('range: 使用的内存字节数:%d ' . PHP_EOL, memory_get_usage(false) - $start);

Generator是中断函数,当foreach循环的时候yield才被执行,每遇到一条yield指令会产生中断并返回,来看看下面的例子。

<?php
function makeDateYield()
{
    file_put_contents('test.log',date('Y-m-d H:i:s', time()), FILE_APPEND);
    yield date('Y-m-d H:i:s', time());
    file_put_contents('test.log',date('Y-m-d H:i:s', time()), FILE_APPEND);
    yield date('Y-m-d H:i:s', time());
    file_put_contents('test.log',date('Y-m-d H:i:s', time()), FILE_APPEND);
    yield date('Y-m-d H:i:s', time());
    file_put_contents('test.log',date('Y-m-d H:i:s', time()), FILE_APPEND);
    yield date('Y-m-d H:i:s', time());
    file_put_contents('test.log',date('Y-m-d H:i:s', time()), FILE_APPEND);
    yield date('Y-m-d H:i:s', time());
    file_put_contents('test.log',date('Y-m-d H:i:s', time()), FILE_APPEND);
    yield date('Y-m-d H:i:s', time());
    file_put_contents('test.log',date('Y-m-d H:i:s', time()), FILE_APPEND);
    yield date('Y-m-d H:i:s', time());
    file_put_contents('test.log',date('Y-m-d H:i:s', time()), FILE_APPEND);
    yield date('Y-m-d H:i:s', time());
    file_put_contents('test.log',date('Y-m-d H:i:s', time()), FILE_APPEND);
    yield date('Y-m-d H:i:s', time());
    file_put_contents('test.log',date('Y-m-d H:i:s', time()), FILE_APPEND);
    yield date('Y-m-d H:i:s', time());
    file_put_contents('test.log',date('Y-m-d H:i:s', time()), FILE_APPEND);
}


$generator = makeDateYield();

foreach($generator as $item) {
    echo $item . PHP_EOL;
    sleep(1);
}
每次执行文件写操作,然后遇到第一条yield指令会执行后中断返回

2.2yield作为接收方的使用

当yield作为接收方的时候,需要声明为表达式。通过Generator的send操作,来向yield表达式发送数据。看下面的例子:

<?php
function TaskReceive()
{
    while(true) {
        doTask(yield);
    }
}

function doTasK($task)
{
    if (0 === strcmp($task, 'q')) {
        exit;
    }
    echo $task . date('Y-m-d H:i:s', time()) . PHP_EOL;
    sleep(1);
}

$generate = TaskReceive();
$generate->send('1');
$generate->send('2');
$generate->send('3');
$generate->send('4');
$generate->send('q');

下面是官方提供的写文件的例子:

<?php
function logger($fileName) {
    $fileHandle = fopen($fileName, 'a');
    while (true) {
        fwrite($fileHandle, yield . "\n");
    }
}

$logger = logger(__DIR__ . '/test.log');
$logger->send('Foo');
$logger->send('Bar');

2.3yield即作为接收方又作为发送方

function gen() {
    $ret = (yield 'foo');
    echo $ret . PHP_EOL;
    $ret =  (yield 'bar');
    echo $ret . PHP_EOL;
}

$gen = gen();
var_dump($gen->current());  //foo
var_dump($gen->send('php')); //php bar
var_dump($gen->send('go')); //go null
当yield作为表达式时,generator会在第二条yield指令或者表达式的地方中断返回

3.PHP协程的实现

3.1什么是协程

协程是用户态的线程,由用户态非抢占式调度,而不是操作系统调度。所以协程的创建和销毁开销对比多线程或者多进程而言会小很多。

3.2协程的实现

之前也介绍过了,实现依赖Generator的中断机制,下面给一个官方的较完善的例子。

<?php
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();
    }
}

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

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

4.参考

https://nikic.github.io/2012/12/22/Cooperative-multitasking-using-coroutines-in-PHP.html

如无特殊说明,文章均为本站原创,转载请注明出处。如发现有什么不对的地方,希望得到您的指点。

发表评论

电子邮件地址不会被公开。 必填项已用*标注