同事甩一句「协程不就是 goroutine」之后,文档页却对不上那套直觉。从 PHP-FPM 迁到常驻进程时,最先撞上的心智模型常常是:请求别再「来一次起一次」,把阻塞 IO 换成可切换的轻量并发单位。旁边写 Go 的人听着省事,落地却容易把调度器、内存模型和生态一起糊过去。下面只把类比停在「轻量并发单位」,差异按文档分开写。
这篇对齐哪套文档
对照 Open Swoole Coroutine 页(页面上可见 OpenSwoole 与 pecl/composer 安装提示)。同日也看了 wiki.swoole.com 中文文档入口(标明面向 Swoole4)。Open Swoole 与 Swoole 是分叉维护的两条线,命名空间、发行渠道不完全相同。下文协程行为按 Open Swoole 该页描述,Swoole 侧只当「同类用户态协程 + IO 切换」的对照,不把两套 API 写成可以无脑替换。
类比能走到哪一步
Open Swoole 文档把 coroutine 说成用户态里的「线程」:创建、销毁、切换开销低,不经过操作系统线程那一套。它不能单靠自己吃满多核,需要进程模型去铺 CPU。调度上,服务端常为每个请求建一个协程,并在 IO 等待时切换——典型是查库、打下游还没回来时,先去干别的。写法上鼓励同步风格,少写多层回调。它也提供 Channel 等在协程间同步。相对 PHP 自己的 yield/generator,文档强调 IO 切换由运行时处理,不必处处手写 yield。
Go 的 goroutine 也是「很多轻量执行体,由运行时往 OS 线程上复用」。到这一层,两边确实像:都是用户态并发单位,阻塞点附近让出执行权,开发者少碰线程池手工调度。
类比停住之后差在哪
运行时绑的语言不一样。Open Swoole 挂在 PHP 扩展 / 常驻服务上:请求生命周期、扩展兼容、全局变量与 static 在切换前后还信不信得过,文档都单独警告——协程共享同一地址空间,切换是隐式的,全局状态可能在你以为「顺序执行」时被别的协程改掉。Go 从语言层面就把 goroutine 当一等公民,标准库、工具链、竞态检测(race detector)围着它转,心智负担分摊方式不同。
调度器也不只是「都会切换」。Open Swoole 页写的是基于 IO 状态的协程调度,并提到 co::run 一类上下文:协程要跑在调度器管得到的上下文里,HTTP/Server 回调里通常已建好,自己 go 时要注意别嵌套错误的 co::run。还有 max_coroutine 一类上限,顶满后新协程建不出来(HTTP 场景文档举例会回到错误响应)。Go 运行时是 M:N,历史上从协作式走到可抢占,和「纯 IO 钩子让出」不是同一张图。把 Go 里「随便 go 出去」的习惯原样搬进 PHP 常驻进程,容易在连接、上下文、扩展线程安全上翻车。
生态客户端也要分开看。Open Swoole 文档列了协程版 TCP/HTTP/Redis/MySQL 等客户端,以及 Runtime Hook:把一批原生 PHP IO 函数钩到协程调度上,让同步写法跑在异步 IO 上。Go 则是标准库 net/http、数据库驱动等直接按 goroutine 模型设计。会写 Go 并发,不等于已经熟悉「哪些 PHP 扩展在 hook 下安全、哪些全局状态不能跨协程」。
常驻内存本身是另一刀。FPM 请求结束基本清空请求级状态。Swoole/Open Swoole 进程活着,连接池、静态缓存、单例会跨请求留下来——这是性能来源,也是泄漏和脏状态来源。goroutine 多并不自动教会你管 PHP 进程里的长寿对象。
怎么用这个对照
把 goroutine 当「协程大概长什么样」的入门比喻可以。据此假设调度语义、取消模型、通道用法、工具链一一对应,不行。读 Open Swoole 协程页时,优先盯:上下文怎么建、IO 何时切换、Channel / WaitGroup、hook 范围、全局变量注意点、max_coroutine。Go 文档用来对照「语言运行时原生并发」长什么样,而不是当 Swoole 配置手册。
你们从 FPM 迁常驻时,协程这一层是先按文档抠切换点,还是先被「跟 Go 一样」带进沟里过?