协程客户端必须在协程上下文中使用,否则报SWOOLE_ERROR_CO_NOT_IN_COROUTINE;on('Request')天然支持,WorkerStart等需用go()或Coroutine::run()显式启动协程环境。
直接在 WorkerStart、Task 或普通 CLI 脚本里 new 一个 SwooleCoroutineMySQL,会报 SWOOLE_ERROR_CO_NOT_IN_COROUTINE 错误。这不是代码写错了,是 Swoole 的硬性约束:所有协程客户端(MySQL、Redis、HttpClient 等)只能在已激活的协程栈中运行。
常见错误场景:
on('WorkerStart') 回调里直接 new 协程客户端go() 外部调用 Co::sleep(1)
new SwooleCoroutineRedis() 但没包在 go() 或 run() 里正确做法:
SwooleCoroutinerun() 包裹整个逻辑,确保入口就是协程环境go(function () { $mysql = new SwooleCoroutineMySQL(); ... });
on('Request') 是天然协程环境,可直接用,不用套 go()
go() 不等于立即并发执行go() 只是把回调注册进协程调度器,并不保证“马上跑”。它依赖底层事件循环(epoll/kqueue)和当前是否有 IO 阻塞点来触发切换。比如下面这段代码:
go(function () { Co::sleep(1); echo "done 1n";});go(function () { Co::sleep(1); echo "done 2n";});echo "main exitn";
输出顺序固定是:main exit → done 1 → done 2(或并行完成,但不会早于 main exit)。因为 go() 启动后主线程继续往下走,Co::sleep(1) 才真正让出控制权。
容易踩的坑:
go() 启动后就能立刻拿到返回值——实际要等协程结束,得用 Channel 或 wait() 同步go() 里做 CPU 密集计算(如大循环、JSON 解析),会阻塞整个线程,其他协程卡住不动Co::sleep()、$mysql->query() 这类 IO 操作才是协程切换的触发点Swoole 协程默认单线程协作式调度,同一时刻只有一个协程在跑,所以全局变量、静态属性、普通数组在协程间天然“隔离”——不是线程安全,而是根本没并发执行机会。但 Go 的 goroutine 是多线程抢占式调度,count++ 这种操作不加锁就会丢数据。
对比示例:
// PHP + Swoole:下面的 $counter 不需要加锁$counter = 0;go(function () use (&$counter) { $counter++;});go(function () use (&$counter) { $counter++;});// 最终 $counter 一定是 2<p>// Go:下面的 count++ 必须加 sync.Mutex,否则大概率是 1 或 2 不确定var count intfor i := 0; i < 2; i++ {go func() {count++}()}
关键区别:
pcntl_fork 或多 Worker 进程时,全局变量就不再是协程安全的了协程只解决 IO 等待问题,对纯计算毫无帮助。Swoole 协程跑在单线程里,一个协程卡在 hash_hmac('sha256', $bigData, $key) 上,整个服务就卡住,HTTP 请求全积压。
真实项目中容易被忽略的点:
json_decode($hugeJson) 在大字符串下耗时显著,建议分块或改用 ext-jsond
Co::getCid() 查当前协程 ID 是调试利器,但别在热路径里频繁调用,有开销最常被低估的其实是协程栈大小——默认 2MB,1000 个协程就是 2GB 内存。如果业务里递归深、闭包多、对象引用复杂,很容易爆栈或内存暴涨,这时得调 Co::set(['stack_size' => 512 * 1024])。