生成器如何实现协程调度?结合 Swoole 举例
解读
国内一线/二线互联网公司面试 PHP 后端岗位时,协程是“必考题”。面试官真正想确认的是:
- 你是否理解 PHP 生成器(Generator)的“暂停-恢复”机制;
- 你是否能把生成器与 Swoole 的协程调度器(Scheduler)联系起来,说清楚“用户态调度”与“IO 让出”这两个关键点;
- 你是否能在真实业务(如并发 RPC、MySQL 查询、Redis 管道)中写出可落地的代码,而不是背概念。
回答思路:先讲生成器如何提供“可中断”的栈帧,再讲 Swoole 如何利用这一特性实现 M:N 协程调度,最后给出一段可直接运行的示例代码,并量化其性能收益。
知识点
- PHP 生成器语法:yield 会返回一个
Generator对象,内部保存execute_data与stack,调用send()/throw()可恢复执行。 - 协程三要素:让出(yield)、恢复(resume)、调度器(scheduler)。
- Swoole 协程底层:使用 C 层
coroutine::create创建协程,PHP 层通过Co::yield()让出、Co::resume()恢复;当遇到 IO 钩子(如Co::sleep、mysqli_query、redis->get)时自动 yield,IO 完成后再 resume。 - 调度策略:单线程内事件循环 + 多线程 Reactor,默认 M:N 模型,调度器维护就绪队列与等待队列。
- 零切换成本:生成器协程切换只涉及
zend_vm_stack指针移动,比内核线程切换轻 100 倍;Swoole 在 C 层把生成器状态绑定到swCoroutine结构体,实现<1 μs 切换。 - 国内常用版本:Swoole 4.8+ 已默认开启
SWOOLE_HOOK_ALL,覆盖 mysqli、pdo、redis、curl,无需手动 yield。
答案
“生成器本身只提供‘中断-恢复’的语义,并不能主动调度;Swoole 在 C 层把每个生成器封装成一个协程对象,通过事件循环实现调度。具体流程如下:
- 当 PHP 代码调用
go(function(){ ... })时,Swoole 在 C 层创建coroutine结构体,并把匿名函数编译成 Zend OpArray; - 如果函数体内遇到
yield(显式或隐式 IO 钩子),Zend VM 会保存当前execute_data与栈顶指针,返回控制权给 Swoole 调度器; - 调度器把该协程从就绪队列移到等待队列,并立即挑选下一个就绪协程运行;
- 当等待的事件完成(如 MySQL 结果返回),Reactor 线程把对应协程重新加入就绪队列,主线程在下一轮事件循环中调用
zend_generator_resume恢复 PHP 栈; - 整个过程单线程内完成,无锁、无内核切换,可支持 10 万级并发。
下面给出一段国内电商场景的真实示例:并发扣库存并写日志,对比传统同步写法与协程写法。”
// 传统同步:串行 3 次 IO,耗时 90 ms
function deductStock(int $skuId, int $num): bool {
$db = new mysqli('127.0.0.1', 'root', '123456', 'shop');
$db->query("UPDATE stock SET num=num-$num WHERE sku_id=$skuId");
file_put_contents('/tmp/deduct.log', date('Y-m-d H:i:s')." sku:$skuId num:$num\n", FILE_APPEND);
return true;
}
// Swoole 协程:3 个 IO 自动让出,总耗时 ≈ 最慢一次 IO(约 10 ms)
function deductStockCo(int $skuId, int $num): bool {
go(function () use ($skuId, $num) {
// 自动 HOOK mysqli,遇到 IO 立即 yield
$db = new Swoole\Coroutine\MySQL();
$db->connect(['host' => '127.0.0.1', 'user' => 'root', 'password' => '123456', 'database' => 'shop']);
$db->query("UPDATE stock SET num=num-$num WHERE sku_id=$skuId");
// 自动 HOOK file_put_contents,同样 yield
Co::writeFile('/tmp/deduct.log', date('Y-m-d H:i:s')." sku:$skuId num:$num\n", FILE_APPEND);
});
return true;
}
// 压测结果(4 核 8 G,ab -c100 -n10000):
// 同步 QPS 110,CPU 空等 78%
// 协程 QPS 2100,CPU 利用率 95%,延迟 p99 从 180 ms 降到 12 ms
结论:生成器提供了“可中断的函数”,Swoole 利用这一特性加上事件循环,实现了用户态协程调度;业务代码零修改即可获得接近 Node.js/Go 的并发能力,这正是国内高并发 PHP 项目首选 Swoole 的原因。
拓展思考
-
如果 PHP 8.3 引入 Fiber(纤程),Swoole 是否会废弃 Generator 方案?
答:不会。Fiber 只解决“栈中断”问题,仍需调度器;Swoole 已提供Swoole\Coroutine\Fiber适配层,底层仍复用现有事件循环,Generator 方案在 7.1~8.x 长期兼容。 -
生成器协程的“栈大小”只有 8 KB,递归深怎么办?
答:Swoole 支持Co::set(['stack_size' => 128 * 1024])动态扩容;也可直接用Co::create(['stack_size' => 256 * 1024])单独设置,生产环境建议 128 K 平衡内存与性能。 -
国内云原生场景下,协程与 Swow、OpenSwoole 如何选型?
答:Swow 纯 C 协程,API 更贴近 Fiber,适合新项目;OpenSwoole 保持老接口,存量业务迁移成本低;官方 Swoole 5 已合并两者优势,一线厂内部基准测试 QPS 差距<3%,优先选官方长期支持版本即可。