Hyperf - 协程

转载自:https://hyperf.wiki/3.1/#/zh-cn/coroutine?id=概念

传统运行模式

传统 PHP-FPM 架构的运作模式,PHP-FPM 是一个多进程的 FastCGI 管理程序,是绝大多数 PHP 应用所使用的运行模式。
假设我们使用 Nginx 提供 HTTP 服务(Apache 同理),所有客户端发起的请求最先抵达的都是 Nginx,然后 Nginx 通过 FastCGI 协议将请求转发给 PHP-FPM 处理,PHP-FPM 的 Worker 进程 会抢占式的获得 CGI 请求进行处理,这个处理指的就是,等待 PHP 脚本的解析,等待业务处理的结果返回,完成后回收子进程,这整个的过程是阻塞等待的,也就意味着 PHP-FPM 的进程数有多少能处理的请求也就是多少,假设 PHP-FPM 有 200 个 Worker 进程,一个请求将耗费 1 秒的时间,那么简单的来说整个服务器理论上最多可以处理的请求也就是 200 个,QPS 即为 200/s,在高并发的场景下,这样的性能往往是不够的,尽管可以利用 Nginx 作为负载均衡配合多台 PHP-FPM 服务器来提供服务,但由于 PHP-FPM 的阻塞等待的工作模型,一个请求会占用至少一个 MySQL 连接,多节点高并发下会产生大量的 MySQL 连接,而 MySQL 的最大连接数默认值为 100,尽管可以修改,但显而易见该模式没法很好的应对高并发的场景。

协程的概念

1.概念

从定义上来说,协程是一种轻量级的线程,由用户代码来调度和管理,而不是由操作系统内核来进行调度,也就是在用户态进行。可以直接的理解为就是一个非标准的线程实现,但什么时候切换由用户自己来实现,而不是由操作系统分配 CPU 时间决定。
具体来说,Swoole 的每个 Worker 进程 会存在一个协程调度器来调度协程,协程切换的时机就是遇到I/O操作或代码显性切换时,进程内以单线程的形式运行协程,也就意味着一个进程内同一时间只会有一个协程在运行且切换时机明确,也就无需处理像多线程编程下的各种同步锁的问题。

2.概念示例

单个协程内的代码运行仍是串行的,放在一个 HTTP 协程服务上来理解就是每一个请求是一个协程,举个例子,假设为 请求 A 创建了 协程 A,为 请求 B 创建了 协程 B,那么在处理协程 A的时候代码跑到了查询MySQL的语句上,这个时候 协程 A 则会触发协程切换,协程 A 就继续等待I/O设备返回结果,那么此时就会切换到 协程 B,开始处理 协程 B 的逻辑,当又遇到了一个I/O操作便又触发协程切换,再回过来从 协程 A 刚才切走的地方继续执行,如此反复,遇到I/O操作就切换到另一个协程去继续执行而非一直阻塞等待。

:在 协程 A 中的MySQL 查询操作必须得是一个异步非阻塞的操作,否则会由于阻塞导致协程调度器没法切换到另一个协程继续执行

3.与线程的区别

协程是一个轻量级的线程,协程和线程都适用于多任务的场景下。

相同点:

  • 都有自己的上下文
  • 可以共享全局变量

不同点:

  • 在同一时间可以有多个线程处于运行状态,但对于 Swoole 协程来说只能有一个,其它的协程都会处于暂停的状态
  • 普通线程是抢占式的,哪个线程能得到资源由操作系统决定,而协程是协作式的,执行权由用户态自行分配

注意事项

1.不能存在阻塞代码

什么是阻塞代码?可以简单的认为绝大多数你所熟知的非 Swoole 提供的异步函数的 MySQL、Redis、Memcache、MongoDB、HTTP、Socket等客户端,文件操作、sleep/usleep 等均为阻塞函数,这几乎涵盖了所有日常操作,那么要如何解决呢?Swoole 提供了协程客户端,同时 Swoole 4.1 之后提供了一键协程化的方法 \Swoole\Runtime::enableCoroutine(),只需在使用协程前运行这一行代码,Swoole 会将 所有使用 php_stream 进行socket操作均变成协程调度的异步 I/O,可以理解为除了 curl 绝大部分原生的操作都可以适用

2.不能通过全局变量储存状态

  • 全局变量:均是跟随着一个 请求(Request) 而产生的,而 Hyperf 的 请求(Request)/响应(Response) 是由 hyperf/http-message 通过实现 PSR-7 处理的,故所有的全局变量均可以在 请求(Request) 对象中得到相关的值;

对于 global 变量和 static 变量,在 PHP-FPM 模式下,本质都是存活于一个请求生命周期内的,而在 Hyperf 内因为是 CLI 应用,会存在 全局周期 请求周期(协程周期) 两种长生命周期

  • 全局周期: ,我们只需要创建一个静态变量供全局调用即可,静态变量意味着在服务启动后,任意协程和代码逻辑均共享此静态变量内的数据,也就意味着存放的数据不能是特别服务于某一个请求或某一个协程;

  • 协程周期:由于 Hyperf 会为每个请求自动创建一个协程来处理,那么一个协程周期在此也可以理解为一个请求周期,在协程内,所有的状态数据均应存放于 Hyperf\Utils\Context 类中,通过该类的 getset 来读取和存储任意结构的数据,这个 Context(协程上下文) 类在执行任意协程时读取或存储的数据都是仅限对应的协程的,同时在协程结束时也会自动销毁相关的上下文数据。

最大协程数限制

Swoole Server 通过 set 方法设置max_coroutine参数,用于配置一个 Worker 进程最多可存在的协程数量。因为随着 Worker 进程处理的协程数目的增加,其对应占用的内存也会随之增加,Hyperf中默认设置为100000

使用协程

创建协程

只需通过co(callable $callable)go(callable $callable)函数或 Hyperf\Coroutine\Coroutine::create(callable $callable) 即可创建一个协程,协程内可以使用协程相关的方法和客户端。

判断当前是否处于协程环境内

Hyperf\Coroutine\Coroutine::inCoroutine(): bool

获得当前协程的 ID

Hyperf\Coroutine\Coroutine::id(): int 获得当前的 协程 ID,如不处于协程环境下,会返回 -1

Channel 通道

Channel 主要用于协程间通讯,当我们希望从一个协程里返回一些数据到另一个协程时,就可通过Channel来进行传递。

主要方法:

  • Channel->push :当队列中有其他协程正在等待pop数据时,自动按顺序唤醒一个消费者协程。当队列已满时自动yield让出控制权,等待其他协程消费数据
  • Channel->pop :当队列为空时自动 yield,等待其他协程生产数据。消费数据后,队列可写入新的数据,自动按顺序唤醒一个生产者协程。
<?php
co(function () {
    $channel = new \Swoole\Coroutine\Channel();
    co(function () use ($channel) {
        $channel->push('data');
    });
    $data = $channel->pop();
});

Defer 特性

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

// 注册一个 defer 回调,在协程结束前会执行此回调
Co::defer(function () use ($resource) {
    echo "正在清理资源...\n";
    // 这里可以进行实际的资源清理操作
    // 例如:fclose($resource); 或者 $dbConnection->close();
});

WaitGroup 特性

WaitGroup 的用途是使得主协程一直阻塞等待直到所有相关的子协程都已经完成了任务后再继续运行,这里说到的阻塞等待是仅对于主协程(即当前协程)来说的,并不会阻塞当前进程。

 /**
     * 测试协程
     */
    public function testCo1()
    {
        $wg = new CoroutineWaitGroup();
        $result = [];
        //计数器加二
        $wg->add(2);
        //创建协程A
        co(function () use ($wg, &$result) {
            $client = $this->clientFactory->create();
            $client->get('127.0.0.1:9501/adminv1/user/test?user=测试1');
            $result[] = '123';
            //计数器减一
            $wg->done();
        });
        //协程B
        co(function () use ($wg, &$result) {
            $client = $this->clientFactory->create();
            $client->get('127.0.0.1:9501/adminv1/user/test?user=测试2');
            $result[] = '321';
            $wg->done();
        });
        //等待协程 A 和协程 B 运行完成
        $wg->wait();
        return $result;
    }

Parallel 特性

Parallel特性是 Hyperf 基于 WaitGroup 特性抽象出来的一个更便捷的使用方法

parallel([],0)
0:表示最大运行的协程数为无限制,可通过限制最大运行数量防止大量协程运行导致宕机等异常情况

 public function testCo3()
    {
        $result = parallel([
            function () {
                var_dump(time());
                return Coroutine::id();
            },
            function () {
                var_dump(time());
                return Coroutine::id();
            }
        ],1);
        return $result;
    }

协程上下文

Hyperf 里实现协程的上下文管理将非常简单,基于 Hyperf\Utils\Context 类的 set(string $id, $value)、get(string $id, $default = null)、has(string $id)、override(string $id, \Closure $closure) 静态方法即可完成上下文数据的管理,通过这些方法设置和获取的值,都仅限于当前的协程,在协程结束时,对应的上下文也会自动释放,无需手动管理,无需担忧内存泄漏的风险。

Hyperf\Utils\Context::set()
// 将 bar 字符串以 foo 为 key 储存到当前协程上下文中
$foo = Context::set('foo', 'bar');
Hyperf\Utils\Context::get()
// 从当前协程上下文中取出 key 为 foo 的值,如不存在则返回 bar 字符串
$foo = Context::get('foo', 'bar');
Hyperf\Utils\Context::has()
// 从当前协程上下文中判断 key 为 foo 的值是否存在
$foo = Context::has('foo');
Hyperf\Utils\Context::override()
// 从协程上下文取出 $request 对象并设置 key 为 foo 的 Header,然后再保存到协程上下文中
$request = Context::override(ServerRequestInterface::class, function (ServerRequestInterface $request) {
    return $request->withAddedHeader('foo', 'bar');
});
posted @   Myifb  阅读(173)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
点击右上角即可分享
微信分享提示