带你学习hyperf-7.1 特性

特性

Channel 通道

类似于 go 语言的 chanChannel 可为多生产者协程和多消费者协程模式提供支持。底层自动实现了协程的切换和调度。 Channel 与 PHP 的数组类似,仅占用内存,没有其他额外的资源申请,所有操作均为内存操作,无 I/O 消耗,使用方法与 SplQueue 队列类似。
Channel 主要用于协程间通讯,当我们希望从一个协程里返回一些数据到另一个协程时,就可通过 Channel 来进行传递。

主要方法:

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

下面是一个协程间通讯的简单例子:

<?php
co(function () {
    $channel = new SwooleCoroutineChannel();
    co(function () use ($channel) {
        $channel->push('data');
    });
    $data = $channel->pop();
});

Defer 特性

当我们希望在协程结束时运行一些代码时,可以通过 defer(callable $callable) 函数或 HyperfCoroutine::defer(callable $callable) 将一段函数以 栈(stack) 的形式储存起来,栈(stack) 内的函数会在当前协程结束时以 先进后出 的流程逐个执行。

WaitGroup 特性

WaitGroup 是基于 Channel 衍生出来的一个特性,如果接触过 Go 语言,我们都会知道 WaitGroup 这一特性,在 Hyperf 里,WaitGroup 的用途是使得主协程一直阻塞等待直到所有相关的子协程都已经完成了任务后再继续运行,这里说到的阻塞等待是仅对于主协程(即当前协程)来说的,并不会阻塞当前进程。
我们通过一段代码来演示该特性:

<?php
$wg = new HyperfUtilsWaitGroup();
// 计数器加二
$wg->add(2);
// 创建协程 A
co(function () use ($wg) {
    // some code
    // 计数器减一
    $wg->done();
});
// 创建协程 B
co(function () use ($wg) {
    // some code
    // 计数器减一
    $wg->done();
});
// 等待协程 A 和协程 B 运行完成
$wg->wait();
注意 WaitGroup 本身也需要在协程内才能使用

Parallel 特性

Parallel 特性是 Hyperf 基于 WaitGroup 特性抽象出来的一个更便捷的使用方法,我们通过一段代码来演示一下。

<?php
use HyperfUtilsExceptionParallelExecutionException;
use HyperfUtilsCoroutine;
use HyperfUtilsParallel;

$parallel = new Parallel();
$parallel->add(function () {
    sleep(1);
    return Coroutine::id();
});
$parallel->add(function () {
    sleep(1);
    return Coroutine::id();
});

try{
    // $results 结果为 [1, 2]
   $results = $parallel->wait(); 
} catch(ParallelExecutionException $e){
    // $e->getResults() 获取协程中的返回值。
    // $e->getThrowables() 获取协程中出现的异常。
}
注意 HyperfUtilsExceptionParallelExecutionException 异常仅在 1.1.6 版本和更新的版本下会抛出

通过上面的代码我们可以看到仅花了 1 秒就得到了两个不同的协程的 ID,在调用 add(callable $callable) 的时候 Parallel 类会为之自动创建一个协程,并加入到 WaitGroup 的调度去。
不仅如此,我们还可以通过 parallel(array $callables) 函数进行更进一步的简化上面的代码,达到同样的目的,下面为简化后的代码。

<?php
use HyperfUtilsCoroutine;

// 传递的数组参数您也可以带上 key 便于区分子协程,返回的结果也会根据 key 返回对应的结果
$result = parallel([
    function () {
        sleep(1);
        return Coroutine::id();
    },
    function () {
        sleep(1);
        return Coroutine::id();
    }
]);
注意 Parallel 本身也需要在协程内才能使用

限制 Parallel 最大同时运行的协程数

当我们添加到 Parallel 里的任务有很多时,假设都是一些请求任务,那么一瞬间发出全部请求很有可能会导致对端服务因为一瞬间接收到了大量的请求而处理不过来,有宕机的风险,所以需要对对端进行适当的保护,但我们又希望可以通过 Parallel 机制来加速这些请求的耗时,那么可以通过在实例化 Parallel 对象时传递第一个参数,来设置最大运行的协程数,比如我们希望最大设置的协程数为 5 ,也就意味着 Parallel 里最多只会有 5 个协程在运行,只有当 5 个里有协程完成结束后,后续的协程才会继续启动,直至所有协程完成任务,示例代码如下:

use HyperfUtilsExceptionParallelExecutionException;
use HyperfUtilsCoroutine;
use HyperfUtilsParallel;

$parallel = new Parallel(5);
for ($i = 0; $i < 20; $i++) {
    $parallel->add(function () {
        sleep(1);
        return Coroutine::id();
    });
} 

try{
   $results = $parallel->wait(); 
} catch(ParallelExecutionException $e){
    // $e->getResults() 获取协程中的返回值。
    // $e->getThrowables() 获取协程中出现的异常。
}

Concurrent 协程运行控制

HyperfUtilsCoroutineConcurrent 基于 SwooleCoroutineChannel 实现,用来控制一个代码块内同时运行的最大协程数量的特性。

以下样例,当同时执行 10 个子协程时,会在循环中阻塞,但只会阻塞当前协程,直到释放出一个位置后,循环继续执行下一个子协程。

<?php

use HyperfUtilsCoroutineConcurrent;

$concurrent = new Concurrent(10);

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

协程上下文

由于同一个进程内协程间是内存共享的,但协程的执行/切换是非顺序的,也就意味着我们很难掌控当前的协程是哪一个(事实上可以,但通常没人这么干),所以我们需要在发生协程切换时能够同时切换对应的上下文。
在 Hyperf 里实现协程的上下文管理将非常简单,基于 HyperfUtilsContext 类的 set(string $id, $value)get(string $id, $default = null)has(string $id)override(string $id, Closure $closure) 静态方法即可完成上下文数据的管理,通过这些方法设置和获取的值,都仅限于当前的协程,在协程结束时,对应的上下文也会自动跟随释放掉,无需手动管理,无需担忧内存泄漏的风险。

HyperfUtilsContext::set()

通过调用 set(string $id, $value) 方法储存一个值到当前协程的上下文中,如下:

<?php
use HyperfUtilsContext;

// 将 bar 字符串以 foo 为 key 储存到当前协程上下文中
$foo = Context::set('foo', 'bar');
// set 方法会再将 value 作为方法的返回值返回回来,所以 $foo 的值为 bar

HyperfUtilsContext::get()

通过调用 get(string $id, $default = null) 方法可从当前协程的上下文中取出一个以 $id 为 key 储存的值,如不存在则返回 $default ,如下:

<?php
use HyperfUtilsContext;

// 从当前协程上下文中取出 key 为 foo 的值,如不存在则返回 bar 字符串
$foo = Context::get('foo', 'bar');

HyperfUtilsContext::has()

通过调用 has(string $id) 方法可判断当前协程的上下文中是否存在以 $id 为 key 储存的值,如存在则返回 true,不存在则返回 false,如下:

<?php
use HyperfUtilsContext;

// 从当前协程上下文中判断 key 为 foo 的值是否存在
$foo = Context::has('foo');

HyperfUtilsContext::override()

当我们需要做一些复杂的上下文处理,比如先判断一个 key 是否存在,如果存在则取出 value 来再对 value 进行某些修改,然后再将 value 设置回上下文容器中,此时会有比较繁杂的判断条件,可直接通过调用 override 方法来实现这个逻辑,如下:

<?php
use PsrHttpMessageServerRequestInterface;
use HyperfUtilsContext;

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

Swoole Runtime Hook Level

框架在入口函数中提供了 SWOOLE_HOOK_FLAGS 常量,如果您需要修改整个项目的 Runtime Hook 等级,比如想要支持 CURL 协程 并且 Swoole 版本为 v4.5.4 之前的版本,可以修改这里的代码,如下。

<?php
! defined('SWOOLE_HOOK_FLAGS') && define('SWOOLE_HOOK_FLAGS', SWOOLE_HOOK_ALL | SWOOLE_HOOK_CURL);

!> 如果 Swoole 版本 >= v4.5.4,不需要做任何修改。

zhaohao

大家好,欢迎来到赵豪博客!赵豪,94年生人,PHP程序员一枚,因为对PHP开发有着相对比较浓厚的兴趣,所以现在从事着PHP程序员的工作。 今天再次开通这个博客,这里将记录我的职业生涯的点点滴滴,感谢来访与关注!如果我的博客能给您带来一些帮助那真是一件非常荣幸的事情~

相关推荐

发表评论

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

微信扫一扫

微信扫一扫

微信扫一扫,分享到朋友圈

带你学习hyperf-7.1 特性
返回顶部

显示

忘记密码?

显示

显示

获取验证码

Close