协程开了还卡,根本原因是混入了未协程化的阻塞调用:sleep()、curl_exec()、PDO直连MySQL、未超时的file_get_contents()等,它们使协程退化为单线程排队;常见漏点在中间件、定时任务、异常处理器及第三方SDK中。

Hyperf 里同步执行卡顿,基本不是协程没开,而是阻塞调用没被协程化——sleep()、curl_exec()、PDO 直连 MySQL、未设超时的 file_get_contents() 这类操作,哪怕开了 task_enable_coroutine => true,也会让整个协程挂起,等同于同步阻塞。
为什么协程开了还卡?常见阻塞点在哪
协程调度器只接管「协程友好的 I/O」,比如 Swoole\Coroutine\Http\Client、Co::sleep()、Db::query()(协程驱动)这些。一旦混入传统 PHP 阻塞函数,协程就退化成单线程排队。
-
sleep()和usleep():必须换成Co::sleep() -
curl_exec():禁用,改用Swoole\Coroutine\Http\Client或Hyperf\HttpClient\HttpClient - PDO 直连 MySQL:不走连接池,无法复用,且阻塞;应统一用
Db::query()或 Eloquent -
file_get_contents('http://...'):底层是阻塞 socket,必须换Co\Http\Client - 第三方 SDK 未适配协程:比如某些微信/支付宝 SDK 内部硬编码
curl_exec(),需重写或打 patch
怎么验证某个操作是否真被协程化了
不能只看配置开了没,得看运行时行为。最直接的办法是加一句 Co::sleep(0.001) 在可疑逻辑前后,再用 strace -p $(pgrep -f 'http_worker') -e trace=epoll_wait 观察是否真正挂起 epoll —— 如果看到大量 epoll_wait 调用且返回超时时间,说明协程在等 I/O;如果长时间没有 epoll_wait,大概率是代码卡在阻塞调用里忙等。
- 日志里出现
PHP Warning: Swoole\Coroutine::sleep(): timeout:说明协程调度正常,但某处耗时过长 - 压测时 CPU 单核跑满、QPS 上不去、
co::stats()显示coroutine_num长期为 1:基本确定存在未协程化的阻塞调用 - 用
hyperf/tracer开启 trace,重点看ext-curl或pdo_mysql是否出现在慢调用链里
哪些地方最容易漏掉协程适配
协程化不是一劳永逸的事,尤其在中间件、装饰器、定时任务和异常处理路径里,很容易写出“看着没问题、跑起来就卡”的代码。
- 中间件里调用
Redis::get()却用了非协程 Redis 客户端(如 phpredis)→ 应确保用的是Hyperf\Redis\Redis实例,它底层走协程连接池 - 定时任务
@Command类里直接 new PDO → 必须改用Db::query(),否则每个任务都新建连接并阻塞 - 全局异常处理器中调用
Log::error()带了大对象 dump → 日志驱动若没配异步写入,会同步刷磁盘,卡住协程 - WebSocket onMessage 中循环发消息却没加
Co::usleep()控制节奏 → 短时间内创建几千个协程,调度器扛不住,表现像卡死
协程调度本身不解决卡顿,它只是把“等”这件事变得可并发。真正要动刀的,是把所有等待行为替换成协程感知的版本——不是“能不能用协程”,而是“你有没有主动让它协程化”。漏掉一个 curl_exec(),整条请求链就退回单线程模型。


















