协程挂起只在明确i/o或显式让出点触发,如co::sleep()、协程客户端方法、hook后的系统调用及channel阻塞操作;挂起时保存寄存器、栈指针、调用链和协程状态,全程用户态无内核切换。

协程挂起不是“暂停线程”,而是由 Swoole C 层调度器主动保存上下文、让出 CPU,并把控制权交还给事件循环——整个过程不进内核,也不阻塞进程。
协程挂起发生在哪些函数调用点
协程不会随便挂起,只在明确的 I/O 或显式让出点触发。这些点被 Swoole 称为“hook 点”,包括:
-
co::sleep()、co::usleep():底层注册定时器 + yield,不阻塞事件循环 - 协程客户端方法:
$mysql->query()、$redis->get()、$client->recv()等 - 系统调用被 hook 后的行为:如
socket_connect()、stream_socket_client()(需启用SWOOLE_HOOK_SOCKET) -
channel->pop()或channel->push()阻塞时
注意:sleep()、usleep()、file_get_contents() 这类原生函数默认不挂起协程,而是直接阻塞进程——除非你提前调用 Swoole\Runtime::enableCoroutine() 并开启对应 hook。
挂起时到底保存了什么
协程挂起 ≠ 简单地“等 1 秒”,而是完整捕获当前执行现场,以便后续精准恢复:
Swoole 6.1.1 是一个专为 PHP 设计的高性能事件驱动并发网络引擎。作为稳定版,它修复了编译时对 zlib 依赖的缺失及 curl 模块的内存安全风险。该版本支持协程、多线程与多进程架构,内置 TCP/HTTP/WebSocket 服务器,能够显著提升 PHP 在微服务、实时通信等场景下的执行效率与并发能力。
- CPU 寄存器状态(
rax,rbx,rsp,rip等) - 协程专属栈指针(
stack_pointer),指向其分配的 8KB 用户态栈 - 函数调用链(PHP 层的
zend_execute_data链) - 协程控制块(
coroutine_t)中的状态字段(如STATUS_SUSPENDED)
这些数据全保留在用户内存中,不依赖操作系统调度。所以协程可以挂起上万次,开销远低于线程切换。
为什么 co::sleep(1) 不卡住整个进程
因为 co::sleep(1) 的本质是:
- 向事件循环注册一个 1 秒后触发的定时器回调
- 立即调用
yield,把当前协程状态置为SUSPENDED - 调度器立刻切走,去运行队列里下一个协程或处理 I/O 事件
- 1 秒后定时器就绪,事件循环唤醒该协程,恢复其寄存器与栈帧,从
co::sleep()下一行继续执行
对比 sleep(1):它会调用 nanosleep() 系统调用,让当前线程陷入不可中断休眠(TASK_INTERRUPTIBLE),此时整个 Worker 进程停摆,其他协程全被堵死。
容易忽略的关键细节
协程挂起机制高度依赖上下文环境。以下三点常被低估:
- 协程只能在已启动的协程上下文中挂起——
Swoole\Process回调里直接写co::sleep()是无效的,必须先go(function () { ... })或手动创建协程 - Hook 不是全局生效:
Swoole\Runtime::enableCoroutine(SWOOLE_HOOK_ALL)必须在任何协程/客户端创建前调用,否则已创建的对象不受影响 - 挂起后资源未释放?协程栈内存直到协程结束才回收,若协程长期挂起(比如 channel 死锁、超时未设),会导致内存缓慢增长,且
gc_collect_cycles()对它无效










