协程中singleton会共享实例,因swoole常驻进程导致静态属性全局可见;应改用coroutine::getcontext()管理请求级状态,连接池替代单例连接。

协程里用 Singleton 会共享实例,不是“每个请求一个”
PHP-FPM 下的单例在每次请求结束就销毁,所以 Singleton::getInstance() 每次都返回新实例(或至少是隔离的)。Swoole 协程 Server 是常驻进程,Singleton 类一旦加载,静态属性 self::$instance 就在 Worker 进程内存里一直存在——所有协程共用同一个实例。
典型现象:A 协程调用 $user = UserContext::getInstance()->setUserId(123),B 协程紧接着调用 UserContext::getInstance()->getUserId(),结果拿到 123,而不是预期的空或自己的 ID。
- 根本原因不是 Singleton 写得不对,而是它本就不该承载请求级状态
- 类中任何静态属性、全局变量、
$_SERVER、$GLOBALS都会被所有协程看到 - 哪怕你加了
__clone或__wakeup,也拦不住协程间共享内存的事实
为什么不能简单把 Singleton 改成“协程局部单例”
有人想绕开问题,比如在 getInstance() 里用 Co::getUid() 做 key 缓存不同实例。这看似可行,但实际踩坑:
-
Co::getUid()返回的是协程 ID,但协程退出后 ID 可能被复用,导致旧数据残留 - 没手动清理机制的话,实例会越积越多,引发内存泄漏
- 无法保证构造时机——比如在
onReceive里首次调用才创建,但中间可能有并发访问竞争 - 如果 Singleton 内部依赖了 PDO 或 Redis 连接,而这些连接没走连接池,直接复用会出错
换句话说:强行给 Singleton 加“协程维度”,等于自己造一套不稳定的上下文管理,得不偿失。
真正安全的替代方案:用 Context + 依赖注入
Swoole 提供了原生支持的协程上下文机制,比自己维护数组靠谱得多:
Swoole 6.1.1 是一个专为 PHP 设计的高性能事件驱动并发网络引擎。作为稳定版,它修复了编译时对 zlib 依赖的缺失及 curl 模块的内存安全风险。该版本支持协程、多线程与多进程架构,内置 TCP/HTTP/WebSocket 服务器,能够显著提升 PHP 在微服务、实时通信等场景下的执行效率与并发能力。
- 用
Swoole\Coroutine::getContext()获取当前协程私有数组,写入请求级数据(如用户 ID、token) - 把 Context 当作“协程内全局变量”,只读写本协程可见范围
- 在 HTTP Server 的
onRequest回调开头,统一初始化 Context:Co::getContext()['user'] = $parsedUser; - 业务逻辑里不再调用
Singleton::getInstance(),而是从 Context 取:$user = Co::getContext()['user'] ?? null;
如果项目已重度耦合 Singleton,可临时封装一层代理:
class RequestContext
{
public static function getUser(): ?User
{
return Co::getContext()['user'] ?? null;
}
}
这样既不动老代码调用方式,又切断了静态变量链。
连接池里的 Singleton 更危险:别让连接跨协程复用
数据库/Redis 单例最容易被误用。例如写个 DbSingleton,内部持有一个 PDO 实例——这在协程里等于让所有请求共用一个连接句柄。
后果很直接:PDO::query() 在协程 A 中执行一半,协程 B 插进来发另一条 SQL,底层 socket 数据就乱了,大概率报错 MySQL server has gone away 或返回错乱结果。
- 正确做法是用连接池:
Swoole\Coroutine\MySQL或Swoole\Coroutine\Redis配合Channel管理连接生命周期 - 连接池对象本身可以是单例(因为它只管分发),但池中每个连接必须严格绑定到单个协程使用完毕再归还
- 切勿在 Singleton 里保存
$this->connection这种实例变量
最易忽略的一点:即使启用了 Swoole\Runtime::enableCoroutine(SWOOLE_HOOK_ALL),也不能拯救错误复用的连接——Hook 只解决调用阻塞,不解决数据竞争。










