协程

基本

一个请求就是一个协程,路由绑定的控制器方法,已经在协程内了

public function index()
{
    if (Coroutine::inCoroutine()){    # true
        return  Coroutine::id();      # int
    };
}

协程 ID一直递增,跟配置中 Constant::OPTION_MAX_COROUTINE 无关

# OPTION_MAX_COROUTINE => 100

for ( $i=0 ; $i < 10000 ; $i++){
    co(function (){
        echo  Coroutine::id().PHP_EOL;    # 10002...
    });
}

可以在任何协程中创建协程,称为子协程。主协程可以阻塞等待子协程执行完毕。但实际上他们没有父子关系,协程之间都是同级的,主协程提前结束,不会影响子协程。协程之间的通信由channel完成

没有等待子协程,直接返回

co(function (){
    sleep(2);
});

return ['ok'];

WaitGroup

阻塞等待子协程

use Hyperf\Utils\WaitGroup;

$w = new WaitGroup();
$w->add(1);            # 需要指定有几个协程需要等待

co(function ()use($w){
    sleep(2);
    $w->done();
});

# 设置超时时间。子任务全部成功完成 返回 1
$rs = $w->wait(1);
if ($rs){
    return ['ok'];
}
return ['err'];     # 只等待了1秒

Parallel

使用WaitGroup特性封装,可以限制Parallel内协程最大同时运行数

$p = new Parallel(1);        // Parallel 内协程最大同时运行数
$p->add(function (){         // 使用 add 创建协程
    sleep(3);
    return Coroutine::id();

});
$p->add(function (){
    sleep(3);
    return Coroutine::id();
});

$rs $p->wait();        //  返回协程内返回的结果 数组
------
$result = parallel([
    function () {
        sleep(1);
        return Coroutine::id();
    },
    function () {
        sleep(1);
        return Coroutine::id();
    }
]);

Channel

主要用于协程间通讯,生产者和消费者模型

# 实例化时可设置队列(size)长度,默认为 1,可 push 1条数据
$c = new Channel(1);
$c->push($rs);        # push 成功返回 1
# 设置消费者超时时间
$c->pop(2);

文档:

  • Channel->push :当队列中有其他协程正在等待 pop 数据时,自动按顺序唤醒一个消费者协程。当队列已满时自动 yield 让出控制权,等待其他协程消费数据
  • Channel->pop :当队列为空时自动 yield,等待其他协程生产数据。消费数据后,队列可写入新的数据,自动按顺序唤醒一个生产者协程。

为了上面两点,我进了好大的坑。

两个协程,一个生产一个消费,队列为2,生产6个,先生产后消费

$c = new Channel(2);
co(function () use ($c){
    for ( $i=0; $i<6; $i++ ){
        echo "开始PUSH".PHP_EOL;
        $c->push(1);
    }
});
co(function ()use($c){
    while(true){
        echo "开始消费".PHP_EOL;
        $rs = $c->pop(2);
        if (!$rs){
            echo "超时结束".PHP_EOL;
            break;
        }
    }
});

运行结果&日志整理


第二个例子 先消费,后生产

$c = new Channel(2);
co(function ()use($c){
    while(true){
        echo "开始消费".PHP_EOL;
        $rs = $c->pop(2);
        if (!$rs){
            echo "超时结束".PHP_EOL;
            break;
        }
    }
});
co(function () use ($c){
    for ( $i=0;$i<6 ;$i++){
        echo "开始PUSH".PHP_EOL;
        $c->push(1);
    }
});

运行结果&日志整理


Concurrent

控制一个代码块内同时运行的最大协程数。基于Channel

use Hyperf\Utils\Coroutine\Concurrent;

$concurrent = new Concurrent(10);
for ($i = 0; $i < 15; ++$i) {
    $concurrent->create(function () {
        // Do something...
    });
}

协程上下文

在协程结束时,对应的上下文也会自动跟随释放掉

use Hyperf\Utils\Context;

$foo = Context::set('foo', default);
$foo = Context::get('foo', default);
$foo = Context::has('foo');

// 从协程上下文取出 $request 对象并设置 key 为 foo 的 Header,然后再保存到协程上下文中
$request = Context::override(ServerRequestInterface::class, function (ServerRequestInterface $request) {
    return $request->withAddedHeader('foo', 'bar');
});

使用 Context

为啥要使用上下文?什么时候用上下文?
目前不是很清楚。文档中提到的几个点

  • 对每个协程的状态处理都需要通过 协程上下文 来管理。
  • 不能通过全局变量储存状态

    • $_开头的变量、global 变量,以及 static 静态属性
  • 状态 可直接理解为会随着请求而变化的值,事实上在 协程 编程中,这些状态值也是应该存放于 协程上下文 中的
  • 对于与请求相关的数据在协程下也是需要储存到 协程上下文(Context) 内的,所以在编写代码时请务必注意 不要 将单个请求相关的数据储存在类属性内,包括非静态属性。

实践

  • 如果要保持请求中数据的修改,需要把修改后的内容写回上下文中的请求接口类上,直接修改是clone的对象