Hyperf 默认的控制器都是走协程吗?
答案是:是的,从执行环境上看,它们都运行在 Swoole 的协程上下文中;但从并发效果上看,只有使用了“协程客户端”的代码才能真正发挥协程的高并发优势。
它的本质是:Hyperf 基于 Swoole Server。当 HTTP 请求到达时,Swoole 会为每个请求创建一个独立的协程 (Coroutine)来执行对应的 Controller 方法。这意味着你的代码天然处于一个轻量级线程环境中。但是,如果你的代码中调用了传统的阻塞函数(如原生curl、PDO、file_get_contents),该协程会阻塞 (Block)当前 Worker 进程,导致其无法处理其他请求,从而退化为类似 PHP-FPM 的单线程串行模式。只有当你使用Hyper 提供的协程组件(如Hyperf\HttpClient、Hyperf\DbConnection)时,IO 操作才会让出 (Yield)CPU,实现真正的高并发。
如果把 Worker 进程比作一个厨师 (Worker):
- 传统 PHP-FPM:是多个厨师,每人只做一道菜。
- 厨师 A 做菜 -> 做完 -> 下班。
- 厨师 B 做菜 -> 做完 -> 下班。
- 特点:隔离好,但资源浪费,切换成本高。
- Hyperf (纯阻塞代码):是一个厨师,一次只做一道菜,且死等食材。
- 厨师接到订单 -> 开始做 -> 发现缺酱油 ->站在原地等送货员 (阻塞 IO)-> 拿到酱油 -> 继续做 -> 完成。
- 后果:在等酱油期间,厨师什么都干不了。如果有 100 个订单,后面的 99 个都要排队等第一个订单的酱油送到。并发度 = 1。
- Hyperf (协程 + 协程客户端):是一个厨师,拥有“分身术” (协程),且会利用等待时间。
- 厨师接到订单 A -> 开始做 -> 发现缺酱油 ->呼叫协程快递 (非阻塞 IO)->立即转身去处理订单 B(CPU 切换/Yield)。
- 订单 B 做到一半,缺盐 ->呼叫协程快递->转身去处理订单 C。
- 酱油送到了 (IO 完成) ->分身回来,继续做完订单 A。
- 后果:一个厨师同时处理几十上百个订单。并发度 = N (取决于 IO 等待时间和 CPU 速度)。
💡 核心洞察:“跑在协程里”不等于“高并发”。关键在于你是否在 IO 等待时“让出”了控制权。阻塞代码会让协程变成“伪协程”。
一、执行模型:Controller 是如何被调用的?
1. 请求入口
- Swoole Server 监听端口,收到 TCP 连接。
- Swoole 解析 HTTP 协议,生成
Swoole\Http\Request。 - Swoole创建一个新的协程(
Swoole\Coroutine::create)。
2. 协程内执行
- 在这个新协程中,Hyperf 的核心中间件 (
CoreMiddleware) 开始执行。 - 路由匹配找到 Controller。
- DI 容器实例化 Controller(如果是单例则复用,但方法调用是在当前协程栈中)。
- 执行 Controller 方法。
- 结论:是的,你的每一行 Controller 代码,都运行在一个独立的 Swoole 协程中。
3. 协程的生命周期
- 开始:请求进入。
- 结束:响应发送完毕,或发生未捕获异常。
- 隔离:每个协程有独立的栈空间,局部变量互不干扰。
二、阻塞陷阱:为什么有时候“协程”不快?
这是新手最容易踩的坑。协程的高并发依赖于“非阻塞 IO”。
❌ 错误示范:阻塞式代码 (Blocking Code)
publicfunctionslowAction(){// 1. 原生 cURL (阻塞!)$ch=curl_init("http://api.example.com");curl_exec($ch);// ⚠️ 当前 Worker 进程在这里卡住,直到远程服务器返回。// 2. 原生 PDO (阻塞!)$pdo=newPDO(...);$pdo->query("SELECT * FROM users");// ⚠️ 如果 DB 慢,整个进程卡住。// 3. file_get_contents (阻塞!)file_get_contents("http://...");}- 后果:虽然代码跑在协程里,但因为底层系统调用是阻塞的,Swoole 无法挂起这个协程去处理其他请求。QPS 瞬间跌到和 PHP-FPM 一样,甚至更差(因为 Swoole 开销)。
✅ 正确示范:协程式代码 (Coroutine Code)
publicfunctionfastAction(){// 1. Hyperf HTTP Client (非阻塞!)$client=make(\Hyperf\HttpClient\Client::class);$response=$client->get("http://api.example.com");// ⚠️ 这里会发生 Yield:Swoole 挂起当前协程,去处理其他请求。// 当数据返回时,Swoole 恢复这个协程。// 2. Hyperf DB (非阻塞!)$users=Db::table('users')->get();// ⚠️ 同样会 Yield,释放 CPU 给其他协程。}- 后果:在等待 API 返回或 DB 查询期间,当前 Worker 进程可以处理成百上千个其他请求。QPS 提升 10-100 倍。
三、如何验证:我的代码是否在“真”协程运行?
1. 检查 CID (Coroutine ID)
在 Controller 中打印协程 ID:
useSwoole\Coroutine;publicfunctionindex(){echo"Current CID: ".Coroutine::getCid()."\n";// 正常情况:每次请求 CID 不同(或复用空闲 CID)// 如果为 -1:说明不在协程环境中(极少见,除非在 CLI 或特殊回调中)}2. 压力测试对比
- 场景 A:Controller 中
sleep(1)(模拟阻塞)。- 启动 1 个 Worker。
- 并发请求 10 个。
- 结果:耗时约 10 秒。串行执行。
- 场景 B:Controller 中
Co::sleep(1)(协程休眠,非阻塞)。- 启动 1 个 Worker。
- 并发请求 10 个。
- 结果:耗时约 1 秒。并行执行。
3. 查看 Swoole 统计
curlhttp://127.0.0.1:9501/status# 如果开启了 StatusHandler观察coroutine_num。如果有并发请求,这个数字应该大于 1。
四、认知牢笼:常见误区
1. 误区:“只要用了 Hyperf,代码就自动变快。”
- 真相:Hyperf 只是提供了协程环境。如果你写阻塞代码,它比 FPM 还慢(因为 Swoole 的额外开销)。
- 对策:彻底摒弃原生阻塞函数,使用 Hyperf 封装的协程组件。
2. 误区:“Controller 是多线程执行的。”
- 真相:默认配置下,每个 Worker 进程是单线程的。并发是通过多进程 + 单进程内多协程实现的。
- 对策:不要使用线程锁 (
pthread),要使用协程锁 (Swoole\Coroutine\Channel或Lock)。
3. 误区:“我可以随便go()开新协程。”
- 真相:在 Controller 中手动
go(function(){ ... })会导致上下文丢失。新协程没有继承 Request 的 Context,获取不到用户信息、DB 连接等。 - 对策:除非你明确知道自己在做什么(如后台异步任务),否则不要在请求链路中随意开启无关联的子协程。使用
defer()或 Hyperf 的AsyncQueue。
4. 误区:“协程没有数量限制。”
- 真相:每个协程占用少量内存(栈空间,默认 8KB-2MB)。如果开启百万级协程,内存会爆。
- 对策:控制并发度,使用连接池限制 DB/Redis 连接数。
5. 误区:“所有 PHP 扩展都支持协程。”
- 真相:只有Hook 了底层 Socket的扩展才支持协程。
- 支持:Swoole 内置客户端、Hyperf 组件、部分启用了
SWOOLE_HOOK_NATIVE_CURL的原生 curl。 - 不支持:某些老旧的 C 扩展、直接调用系统 blocking API 的代码。
- 支持:Swoole 内置客户端、Hyperf 组件、部分启用了
- 对策:查阅 Swoole 文档,确认扩展的协程兼容性。
🚀 总结:原子化“Hyperf 协程执行”全景图
| 维度 | 关键点 |
|---|---|
| 执行环境 | 每个请求在一个独立的 Swoole 协程中运行 |
| 并发关键 | 必须使用非阻塞 IO (协程客户端) 才能发挥优势 |
| 阻塞后果 | 退化为串行执行,性能低于 FPM |
| 验证方法 | Coroutine::getCid(),压测对比sleepvsCo::sleep |
| 常见陷阱 | 原生 curl/PDO、手动 go() 丢失上下文、扩展不兼容 |
| PHP 隐喻 | Green Threads with Cooperative Multitasking |
| 公式 | Concurrency = (Non_Blocking_IO × Yield_Frequency) ^ Worker_Count |
终极心法:
Hyperf 协程的本质,是“在单线程中模拟并发的艺术”。
别被“协程”二字迷惑,要关注“阻塞”与否。
让出 CPU,才能赢得时间。
于挂起中见并发,于非阻塞见效率;以让渡为尺,解独占之牛,于高并发工程中,求流动之真。
行动指令:
- 审查依赖:检查项目中是否使用了原生
curl、PDO、redis扩展。替换为 Hyperf 对应的协程组件。 - 开启 Hook:在
config/autoload/server.php中,确保settings里开启了hook_flags(如SWOOLE_HOOK_ALL),这样即使部分原生函数也能被协程化。 - 压测验证:写一个简单的睡眠接口,分别用
sleep(1)和Co::sleep(1)测试 QPS,直观感受差异。 - 思维升级:记住,在 Swoole/Hyperf 世界里,阻塞是罪恶。每一次阻塞,都是在浪费服务器的生命。
