第20课:PHP协程

一看到协程立马想到了python,PHP现在也支持协程了,所谓协程本质上也是异步非阻塞技术,它是将事件回调进行了包装,让程序员看不到里面的事件循环。你可以很无脑的把他理解成多个服务程序的并发执行,充分利用CPU的资源,虽然其实质很复杂,但是我不想告诉你那么多,不能知道的太多了,邪恶的笑.....

协程现在有现成的扩展,ptask,不想过多的介绍,以后可能会有更好的扩展了。总之,如果你想用PHP的协程,首先搜搜网上现成的扩展,如果你想写原生的代码,会发现有点痛苦啊
使用ptask第一步当然是安装扩展
$ git clone https://github.com/liexusong/ptask
$ cd ptask/libtask
$ make
$ sudo make install
$ cd ../ext
$ phpize
$ ./configure --with-php-config=path-to-php-config
$ make
$ sudo make install
安装成功之后,我给出了一个简单的例子:
<?php
 
function handler($arg)
{
    for ($i = 0; $i < 1000; $i++) {
        echo $arg, ": ", $i, "\n";
        ptask_yield();
    }
}
 
 
ptask_create("handler", "godeye1");
ptask_create("handler", "godeye2");
 
ptask_run();
上面就是2个handler在执行的代码

还有个扩展也很好,叫swoole,但是它使用起来稍微复杂点,看各位大神的耐心了。
比如swoole协程的例子
$serv = new swoole_server("127.0.0.1", 9502);
$serv->set(array('task_worker_num' => 4));
$serv->on('Receive', function($serv, $fd, $from_id, $data) {
    $task_id = $serv->task("Async");
    echo "Dispath AsyncTask: id=$task_id\n";
});
$serv->on('Task', function ($serv, $task_id, $from_id, $data) {
    echo "New AsyncTask[id=$task_id]".PHP_EOL;
    $serv->finish("$data -> OK");
});
$serv->on('Finish', function ($serv, $task_id, $data) {
    echo "AsyncTask[$task_id] Finish: $data".PHP_EOL;
});
$serv->start();

对于大部分程序员,会用就可以了,不用再往下看,如果有人想探求其原理,我这里也班门弄斧一下:

PHP协程的实现依赖于Generators即生成器,对生成器不理解的可以先网上搜搜,这里不会说过多的基本概念。要说的一点是如果你要遍历一个超大的数组,可能你机器内存都不够用的,这个时候,如果每次都处理数组的一个元素,流式处理整个数组,就不需要那么多内存了,而生成器就是这么个方案
在PHP中Generator是由函数生成的,但这个函数又跟普通的函数不同,Generator提供了一种方便的实现简单的Iterator(迭代器)的方式,使用Generator实现Iterator不需要创建一个类来继承Iterator接口
<?php
function gen() {
    yield 1;
}
$g = gen();
echo $g->valid();    //1
echo $g->current();  //1

echo $g->next();

echo $g->valid();    //
echo $g->current();  //
执行上面代码,看看结果是什么,然后想想为什么
php中,yield关键字只能在函数中使用,而且使用了yield关键字的函数都会返回一个Generator对象
yield语句有点像return语句,代码执行到yield语句,generator函数的执行就会终止,并且会返回yield语句中的表达式的值给Generator对象,这跟return语句一样,不同的是,这返回值只是作为遍历Generator对象的当前元素,而不能赋值给其他变量

多个yield语句
<?php
function gen() {
    yield 1;
    yield 2;
    yield 3;
}
$g = gen();
echo $g->valid();
echo $g->current();
echo "\n";
echo $g->next();
echo $g->valid();
echo $g->current();
echo "\n";
echo $g->next();
echo $g->valid();
echo $g->current();
echo "\n";
echo $g->next();
echo $g->valid();
echo $g->current();

Generator对象的中可迭代的元素就是所有yield语句返回的值的集合,在这个示例中是[1,2,3]。看起来跟数组很像,但它跟数组有本质的区别,遍历Generator对象的每次迭代都只会执行前一次yield语句之后的代码,而且碰到yield语句就会返回一个值,相当于从generator函数中返回,这有点像挂起一个进程(线程)的执行(yield在很多语言中就是用于挂起进程(线程)),然后又启动它继续执行,周而复始直到进程(线程)执行中止,这也是为什么Generator可以用于实现协程的原因

<?php
function gen() {
    $ret = (yield 'yield1');
    var_dump($ret);
    $ret = (yield 'yield2');
    var_dump($ret);
}
$g = gen();
var_dump($g->current());
var_dump($g->send('ret1'));
var_dump($g->send('ret2'));

上面的代码首先是调用函数gen生成一个Generator对象,然后调用这个对象的current方法返回第一个值,显然它是第一个yield语句的返回值,也就是'yield1',这个时候gen函数的执行就会被中止,接着执行var_dump($g->send('ret1'));。

调用$g->send('ret1'),传入参数为字符串'ret1',按照上面的说明,它会赋值给第一个yield表达式,也就是(yield 'yield1')中的yield(注意:这个时候不包括'yield1'),它的值为'ret1',然后会赋值给$ret,所以第二个输出'ret1'就是gen函数中的第一个var_dump输出的。此时对Generator对象的迭代会恢复继续执行,实际上就是调用了一次next函数,它会执行到下一个yield语句:yield 'yield2',这个语句会返回'yield2',它会作为$g->send('ret1')的返回值,所以函数外第二个var_dump会输出'yield2'。

最后再次调用send函数,这次传入的参数为字符串'ret2',跟上面一样,Generator对象当前位置的元素是在gen函数的第二个yield上,所以'ret2'会被传递给第二个yield表达式,也就是作为(yield 'yield2')中的yield的值,并且会被赋值给$ret变量,然后gen函数恢复执行,它会执行gen函数中的最后一个var_dump,此时对Generator对象$g的遍历也结束了,第二个send函数的返回值为NULL,这也是函数外的最后一个var_dump的输出。

再看一个示例:
<?php
function nums() {
    for ($i = 0; $i < 5; ++$i) {
                //get a value from the caller
        $cmd = (yield $i);
        if($cmd == 'stop')
            return;//exit the function
        }     
}
$gen = nums();
foreach($gen as $v)
{
    if($v == 3)//we are satisfied
        $gen->send('stop');
    echo "{$v}\n";
}

在这个示例中对nums函数返回的Generator对象的遍历就是从nums函数中获取数据,这相当于从generator函数传递数据给Generator对象,而当Generator对象可以'stop'传递给nums函数来要求终止Generator的遍历了,这相当于从Generator对象到generator函数的通信