协程调度器是单线程事件循环上的用户态工头,仅在I/O就绪、定时器触发或主动让出时切换协程;死循环会阻塞整个Worker,必须用Co::sleep或yield让渡控制权;切换开销约50ns,栈默认8KB易溢出;Hook是调度前提,未hook的系统调用仍阻塞;全局变量跨协程共享,需手动隔离。

协程调度器本质是单线程事件循环上的用户态工头
它不依赖操作系统调度,也不开新线程,而是在每个 Worker 进程的主线程里跑一个 EventLoop,所有协程都挤在这个线程里排队干活。所谓“调度”,就是这个工头在 I/O 就绪、定时器触发或协程主动让出时,从就绪队列里挑一个协程恢复执行。
常见错误现象:协程写了个死循环(比如 while(true) { $i++; }),整个 Worker 就卡住,其他协程完全没机会跑——因为没 I/O、没 yield、没定时器,调度器根本没插手机会。
- 协程只有遇到
Co::sleep、channel->pop、Co::MySQL->query等 hook 化 I/O 才会挂起 -
usleep、sleep、纯计算循环不会触发调度,必须手动加Co::yield()或改用Co::sleep(0) - Hyperf 等框架会在请求入口自动创建协程,但 CLI 脚本里调了
go()后必须留一句Co::sleep(0.001)或Swoole\Event::wait(),否则进程退出、协程直接丢弃
协程切换靠寄存器+栈指针保存,不是线程上下文切换
每次挂起时,Swoole C 层只保存当前 CPU 寄存器状态和栈指针位置,然后跳转到调度器逻辑;恢复时再把寄存器和栈指针原样载入。整个过程在用户态完成,耗时约 50ns,远低于线程切换的 1μs。
容易踩的坑:协程栈默认 8KB(可配),但递归过深或大数组局部变量会直接栈溢出,报错类似 Fatal error: Allowed memory size exhausted 或静默崩溃,且无法被 try/catch 捕获。
Swoole 6.1.1 是一个专为 PHP 设计的高性能事件驱动并发网络引擎。作为稳定版,它修复了编译时对 zlib 依赖的缺失及 curl 模块的内存安全风险。该版本支持协程、多线程与多进程架构,内置 TCP/HTTP/WebSocket 服务器,能够显著提升 PHP 在微服务、实时通信等场景下的执行效率与并发能力。
- 不要在协程里递归调用自己超过百层
- 避免在协程函数内定义超大局部数组(如
$data = array_fill(0, 100000, 0)) - 用
Co::set(['stack_size' => 2 * 1024 * 1024])可调大,但别滥用——栈多了内存压力反而上升
Hook 是调度器的“眼睛”,没它协程就等于裸奔
调度器本身不干涉代码执行流,它只响应被 Hook 的系统调用。比如 file_get_contents() 默认仍是阻塞的,除非你提前调了 Swoole\Runtime::enableCoroutine(SWOOLE_HOOK_FILE),否则它会直接卡死整个 Worker。
典型误用场景:在 onRequest 回调里用了 curl_exec() 却没启用 SWOOLE_HOOK_CURL,结果并发一上来,所有请求串行排队,QPS 断崖下跌。
-
SWOOLE_HOOK_ALL最省心,但有兼容风险(某些 C 扩展绕过 hook) - 生产环境建议按需开启:
SWOOLE_HOOK_STDIO | SWOOLE_HOOK_CURL | SWOOLE_HOOK_FILE - 验证是否生效:在协程里跑
sleep(1),看其他协程是否也被阻塞——阻塞说明 hook 没启对或启太晚
协程间共享全局状态,隔离的是栈不是内存
每个协程有独立栈,但 $_SERVER、static $cache、global $config 这些全是进程级共享。一个协程改了 $_GET['token'],下一个协程进来可能读到脏值。
最容易被忽略的一点:协程调度依赖底层 epoll/kqueue,而这些机制对普通文件(如 fopen('/tmp/log.txt'))不生效——即使开了 SWOOLE_HOOK_FILE,读写本地文件仍是阻塞的,只是被模拟成“协程友好”而已,实际未提升并发能力。
- 跨协程传数据优先用
Co::getContext()或闭包捕获 - 缓存类逻辑慎用
static,改用Co::getUid()做键隔离 - 日志写文件要走
Co::writeFile()或投递到 TaskWorker,别在协程里直接fwrite()

















