php 协程 yield

什么是协程

  • 理解协程之前最好要理解进程和线程,这里不过多解释,简单来说,进程是资源分配的最小单位,线程是进程中一个单一的执行流,线程共享进程资源,每个线程都有自己独立的栈空间。线程相对于进程而言更加轻量,操作系统调度进程切换的代价很大,需要保存当前进程的各种信息,PCB 进程控制块。线程切换相对更加容易,线程同属于一个进程,只需要切换栈空间。多线程更能利用多核的 cpu,发挥性能。
  • 协程呢,可以说是断点,就如同程序调试时的断点一样,协程可以打破程序顺序执行的特征,协程可以帮助我们调整代码的执行顺序,来实现任务协同工作。网上有好多协程与线程的对比文章,有的说协程比线程好,有的则说线程好,我也尝试了 php 中的协程,使用 yield 关键字。通过我的尝试以及我的理解,我认为二者各自优点,但是我还是更加倾向于线程。我觉得线程时任务并行成为可能,而协程只是改变了代码的执行顺序,能用协程做到的,我们可以通过调整代码的顺序来实现相同的效果。
  • 还有一点就是,有人说协程可以实现异步,我却没有发现协程这方面的作用,我觉得可以用协程调度程序任务,然后用线程并行来实现异步,但是目前我还不会

php 中协程的使用

  • php 中的协程就是在函数中使用 yield 关键字,就可以成为一个生成器来实现协程,生成器是 Geneator 的一个实例对象,所以最好先理解一下迭代器提供的几个抽象函数,rewind(), current(), next(), valid()比如下面一个简单的例子
<?php

function go(){
    yield;
    echo 'yield ';
    yield;
    echo 'end';
};

$test = go();

假如函数中没有那两个 yield 关键字,上述代码会输出 yield end,可是加上 yield,会发现程序什么输出都没有,这是因为 yield 相当于一个断点,函数走到这里会停止,让出 cpu ,这也是协程的工作原理

  • 包含 yield 的函数是生成器函数,不同于普通函数,生成器函数被调用时,实际是返回了一个可迭代的对象,是 Geneator类 的一个实例,迭代对象通过 next() 方法可以遍历到下一个对象,对于生成器,可以使用 next() 实现程序的继续执行。
    还是上面的例子,详细解释一下如何工作,加深自己的理解
<?php
$yield= function(){
    yield;
    echo 'yield ';
    yield;
    echo 'end';
};
$test = $yield();// 生成一个Geneator 的实例对象,对象实例化的时候,会执行一次 rewind() 相当于生成器函数执行到第一次 yield 处中断函数
$test->next();// 输出 yield 调用 next() 就是使程序继续执行,执行到下一个 yield 处再次中断,所以会输入 yield
$test->next();// 输出 end 同上,这个执行后,函数内部不再有中断点
var_dump($test->valid()); // 这里会输出 false 说明迭代结束
  • 下面再介绍一个函数 send(),可以实现我们与生成器的通信。先说一下总结的结论,send() 与 next() 都能让生成器向后迭代一步,在函数中表现出来就是让函数再向下执行,直到遇到下一个 yield。next() 比较直接,就是调度生成器执行至下一个断点处,没有返回值,send() 相对而言增加了通信效果,send() 发送数据至生成器,被 yield 关键字接收,可以赋值给变量,同时执行至下一处断点 yield 处,将 yield 右边的数据作为返回值返回。
  • 相比来说,send()相当于执行 next() 后执行 current();next() 相当于 send(null),而且不取返回值;虽然这样表达不是很准确

    还是写几个例子便于理解掌握
<?php

// 上面的例子中,只单独使用了 yield 关键字,其实 yield 左右两边都是可以跟表达式和变量的
// 先来一个简单的便于理解

function work(){
    $str = yield 'first yield';
    echo $str;
     yield 'end';
}

$s = work();
var_dump($a->current()); // string(11) "first yield"
var_dump($s->next()); // NULL

// 如果将上面的 var_dump($s->next()) 换成 $s->send('php') 会输出 php 并得到 string(3) "end"

迭代器的 current() 以及 send() 会返回当前 yield 右侧的值,如果右侧没有,则返回空,next() 则是继续上一个断点,继续执行到下一个 yield 或者迭代器结束的地方,比如 return 语句,生成器的迭代是通过 valid() 来判断的,valid() 返回 false 时说明迭代结束

<?php

$work = function(){
    $i = 0;
    while(++$i < 10){
        $str = yield $i;
        var_dump($str);
    }
};

// case 1
$test = $work();
foreach($test as $v){
    echo $v,PHP_EOL;
}

// 上面的foreach 实际上就是
foreach($test->rewind(),$v = $test->current();$test->valid();$test->next(),$v = $test->current()){
    echo $v,PHP_EOL;
}
// case 2
$i = 0;
$test = $work();
while($test->valid()){
    $test->send('send '.$i++);
}

// 上述两段代码输出不同 能看出 next() 就是仅仅向下执行,不传入数据
// send() 会传入数据,在生成器中被 yield 接收,用来传递给左侧变量(没有赋值操作的话的话 send() 也就没有使用的意义)

一个更能看出的例子

<?php

$s = (function($num){
    while(--$num){
        var_dump(yield);
    }
})(10);

$i = 0;
while($s->valid()){
    //$s->next();
    $s->send(++$i);// 试着将这条语句注释,执行上方的语句,看看有什么不同的效果
}

除了可以设置生成器每次迭代的值之外,也可以设置键

<?php

function work($i){
    while(--$i > 0){
        yield $i * $i => $i;
    }
}

$test = work(5);
while($test->valid()){
    echo $test->key(),'----',$test->current(),PHP_EOL;
    $test->next();
}

如果不使用 $key => $value 的格式,右侧的变量只能作为 value,键值默认为数字索引

使用协程写两个例子

<?php

const NUM = 10; // 设置生产个数,作为终止条件

$consumer = (function(){
    while(true){
        $receive = yield; // 设置断点
        echo 'receive '.$receive,PHP_EOL;
        echo 'consume end',PHP_EOL;
    }   
})();

$producer = (function($num) use ($consumer){
    echo 'start produce',PHP_EOL,'-----',PHP_EOL;
    while(--$num > 0){
        echo 'produce num '.$num,PHP_EOL;
        $consumer->send('produce num '.$num); // 生产后通知 consumer 消费
    }
    echo '------',PHP_EOL,'produce end';
})(NUM);

再写一个之前线程实现的 线程同步打印 ABC

<?php

/**
 * 实现方式,A 打印后通知 B ,B 打印后通知 C,C 打印后返回 A 处,继续执行
 * 所以将 B 和 C 用生成器实现,A 调用 B,B 调用 C 
 */

const NUM = 10; // 打印次数
const SLEEP_TIME = 1e6; //休眠时间

$c = (function(){
    while(true){
        yield;
        echo 'C';
        usleep(SLEEP_TIME);// 为了看出效果,延时一下
    } 
})();

$b = (function() use ($c){
    while(true){
        yield;
        echo 'B';
        usleep(SLEEP_TIME);
        $c->next();
    } 
})();

$a = (function($num) use ($b){
    while (--$num > 0) {
        echo 'A';
        usleep(SLEEP_TIME);
        $b->next();
    }
    echo PHP_EOL,'print end';
})(NUM);

个人见解

通过对协程的使用,我还是认为协程只是调整了代码的执行顺序,(能通过协程实现的,我们可以自己改写代码顺序来实现),并不能实现并行。协程切换的代价小,但是依旧不能取代线程的作用,协程如果能配合线程,来实现异步,将任务分发给线程,通过协程调度任务,应该会是一个不错的方案

猜你喜欢

转载自www.cnblogs.com/mlover/p/11134493.html