Hyperf - 协程
传统运行模式
传统 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
类中,通过该类的get
、set
来读取和存储任意结构的数据,这个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');
});
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!