PHP 8.1 引入 Fibers 之后,很多人问:PHP 终于能写协程了?我的答案是:能,但别把它当成 Go 的 goroutine。最近在一个数据聚合服务里用 Fibers 做了异步编排,分享几点落地经验。

一、Fibers 不是并行,是协作式挂起。Fiber 只有在显式调用 Fiber::suspend() 时才会让出执行权,不会自动切换。我们的场景是同时调 5 个下游接口:每个 Fiber 里发请求后挂起,等所有 Fiber 都挂起,再由调度器统一收结果。实测下来,5 个 200ms 的接口从串行 1 秒降到 260ms 左右,效果是实打实的。

二、调度器要自己写,而且要写对。最简单的调度器就是一个循环:resume 每个 Fiber,收集已完成的,挂起的继续等。但要注意两点:Fiber 抛异常时,异常会在 resume() 的调用点抛出,调度器必须 try/catch 每个 resume,否则一个下游挂了整个聚合就崩;另外要设总超时,某个 Fiber 一直不恢复,调度器不能死等。

三、异常处理比同步代码更绕。Fiber 内部的异常不会自动冒泡到主流程,只在 resume 时抛出。我的做法是每个 Fiber 的入口包一层 try/catch,把异常转成结构化结果(code + message)返回,调度器按结果决定降级还是重试。这样主流程的错误处理逻辑和同步代码基本一致,review 的时候不容易漏。

四、别在 Fiber 里用阻塞 IO。Fibers 只是挂起机制,本身不提供异步 IO。在 Fiber 里调 file_get_contents() 这种阻塞调用,挂起的是 Fiber 不是 IO,整个进程还是会被堵住。正确姿势是配合非阻塞客户端(比如 curl_multi 或 ReactPHP 的事件循环),在等待 IO 的地方 suspend,等事件循环回调时再 resume。

五、调试体验要提前想好。Fiber 的调用栈在 suspend/resume 之间是断开的,xdebug 的单步跟踪会跳来跳去。我的做法是给每个 Fiber 打上 trace id,日志里全程带上,出了问题靠日志定位,别指望单步调试。

最后问大家一个问题:你们在 PHP 里做并发/异步的时候,是用 Fibers + 事件循环、Swoole/Workerman,还是直接上消息队列拆成异步任务?什么场景下你们会选 Fibers?