面试官考察Swoole协程重在实战经验:需明确go与Coroutine::create行为差异、Co\MySQL不可复用连接、协程变量不能跨go共享;主协程结束会导致$response失效,须用Co::waitGroup等同步;协程客户端须按需创建、禁止长生命周期存储;$this等上下文不继承,需use显式传参;Worker重启后static变量残留易致内存泄漏。

Hyperf 面试中问到 Swoole 协程,不是考你背概念,而是看你有没有踩过坑、调过调度、改过并发逻辑。真正在生产里用过的人,一开口就能说出 go 和 Coroutine::create 的行为差异、Co\MySQL 为什么不能复用连接、协程变量为什么不能跨 go 共享——这些才是面试官想听的。
协程生命周期和 go() 调用时机不对,请求就“丢”了
很多人写 go(function () { $response->end('ok'); }); 就以为完事了,结果接口返回空或超时。问题出在:协程启动后立即返回,主协程(即当前请求协程)可能已经结束并销毁了 $response 对象。
- Hyperf 中每个 HTTP 请求默认运行在一个独立协程里,该协程结束 = 响应已发送/已超时
-
go()启动的是子协程,但父协程不等它,自己先跑完了 - 正确做法是用
Co::waitGroup()或Co::channel()显式同步,或直接在主协程里串行执行(对简单场景) - 更安全的写法是把异步逻辑封装进服务方法,由框架的协程上下文自动管理生命周期
数据库/Redis 连接在协程内必须“按需创建、用完即弃”
常见错误是把 Swoole\Coroutine\MySQL 实例存在类属性或 static 变量里,下个请求进来就复用——这会触发 ERROR: connection is closed 或数据错乱。
- 协程不是线程,没有“连接池自动绑定”这种魔法;Swoole 的协程客户端(如
Co\MySQL、Co\Redis)本身不带连接池,每次new都新建连接 - Hyperf 的
hyperf/database组件才内置连接池,它会在协程 ID 变化时自动切换连接,但前提是必须通过 DI 容器获取ConnectionInterface - 手动 new 协程客户端时,务必确保:只在当前协程内使用、不跨协程传递、不存为长生命周期变量
- 查日志时注意
coroutine id字段,同一请求不同协程 ID 出现相同连接操作,基本就是复用错了
协程上下文丢失:$this、静态变量、全局状态全不可靠
在 go(function () { var_dump($this); }); 里访问控制器实例,大概率报 Fatal error: Using $this when not in object context。这不是 bug,是设计使然。
-
go()创建的新协程没有继承父协程的 PHP 执行上下文(包括$this、global、static变量作用域) - 所有需要的数据必须显式
use进去,且注意引用传递风险:use (&$var)在协程间共享变量极易引发竞态 - Hyperf 的注解(如
@Inject)、中间件、AOP 切面都依赖协程上下文(Context::get()),一旦脱离主协程链路,这些机制就失效 - 调试时可用
Co::getCid()确认当前协程 ID,对比日志中各操作的 CID 是否一致
Worker 进程重启后,协程变量不会清空,但连接和资源会断
很多人以为“协程是轻量级的,所以重启 Worker 没影响”,其实不然。Swoole 的 Worker 进程常驻内存,里面所有 PHP 变量(包括 static、global、单例对象)都会保留,直到进程被 kill。
- 连接类资源(MySQL、Redis、HTTP 客户端)在进程重启时会被关闭,但变量还挂着,下次调用就会抛出“connection closed”异常
- Hyperf 的
OnWorkerStart事件适合做连接池初始化,但要注意:协程客户端不能放在这里 new,得在每次协程内 new - 真正需要“进程级初始化”的,比如配置缓存、文件句柄、第三方 SDK 实例,才适合放在
OnWorkerStart - 热更新(
reload)时,旧 Worker 会优雅退出,新 Worker 启动,但static变量不会自动重置——这是最隐蔽的内存泄漏源之一
协程调度本身不难理解,难的是它把“什么时候变量还活着”“哪个协程能访问哪块内存”“连接到底属于谁”这些原本由 PHP-FPM 隐式保证的事,全摊开让你自己管。面试时聊得越细,越说明你真在线上扛过流量、修过半夜告警。


















