对象复用是swoole常驻进程模型下应对内存与性能压力的必要手段,须在onworkerstart中初始化连接池或单例实例,避免请求级new导致栈内存堆积、gc延迟、连接泄漏及状态不一致。

对象复用不是“能省就省”的权宜之计,而是Swoole常驻进程模型下必须面对的内存与性能现实——频繁创建对象在协程中会快速堆积栈内存、触发GC压力,并可能隐式持有连接或闭包引用,导致连接泄漏或内存持续增长。
onWorkerStart里new一次 vs 每次请求都new
在Swoole中,onWorkerStart是worker进程初始化的唯一稳定时机。这里new一个Redis客户端、数据库连接池管理器或配置类,它就会常驻该worker内存中,后续所有协程请求都可复用这个实例;而若在onReceive、onMessage等回调里每次new,等于每个请求都新建一套对象+关联资源(如TCP socket、PDO句柄),这些对象虽在协程结束时被GC标记,但:
- PHP GC不会立刻回收,尤其当对象含
__destruct或绑定到静态属性时,可能滞留多个协程周期 - 底层连接(如
Co\Redis)若未显式close(),连接不会释放,连接池统计失真 - 重复加载类定义、解析注解、初始化默认属性,带来CPU开销
单例模式在Swoole里为什么容易出错
传统单例(self::$instance ??= new static())在Swoole中看似方便,但极易踩坑:
- 它依赖静态变量,而Swoole多worker进程间不共享内存,每个worker都有独立的
self::$instance副本——这本身没问题,但开发者常误以为“全局唯一”,结果在不同worker里拿到不同状态的实例 - 若单例内部缓存了用户数据(比如
$cache[$uid] = $data),这个缓存只对当前worker生效,横向扩展时数据不一致 - 更危险的是,某些单例在
__construct里做了阻塞操作(如同步读配置文件),会卡住整个worker,影响所有协程
真正安全的做法是:把单例逻辑放在onWorkerStart中手动控制生命周期,并明确注释“此实例仅限本worker内复用”。
Swoole 6.1.1 是一个专为 PHP 设计的高性能事件驱动并发网络引擎。作为稳定版,它修复了编译时对 zlib 依赖的缺失及 curl 模块的内存安全风险。该版本支持协程、多线程与多进程架构,内置 TCP/HTTP/WebSocket 服务器,能够显著提升 PHP 在微服务、实时通信等场景下的执行效率与并发能力。
协程内new对象的栈内存开销不可忽视
每个协程默认分配8KB栈空间(可通过swoole_set_process_name和coroutine::create参数调整),而new一个带10个属性的普通对象,其栈上至少要存放局部变量引用、调用栈帧。如果在高频协程路径(如消息广播循环)里new StdClass()或new DTO(),叠加数千并发协程,栈内存占用会远超预期。
- 实测:1000个并发协程,每个协程
new一个含5个字符串属性的对象,额外增加约2.3MB内存(不含堆分配) - 更隐蔽的问题是,这类对象若被闭包捕获(如
function () use ($obj) { ... }),会导致整个对象无法被GC及时清理 - 推荐做法:用数组代替轻量DTO;对必须用的对象,提前在
onWorkerStart中预建对象池(new SplFixedArray(100)),协程中array_shift复用
连接类对象(Co\Redis / Co\Mysql)必须复用
这是最不能妥协的场景。像Co\Redis这类对象,内部封装了协程友好的socket连接,它的复用不是为了省CPU,而是为了保连接:
- 每次
new Co\Redis()+connect(),都会新建一个TCP连接;而Swoole连接池的设计前提是“连接可归还”,不是“对象可丢弃” - 若在协程里
new Co\Redis()后没调用close(),该连接会一直挂在当前协程栈上,直到协程退出——但协程退出不等于连接关闭,连接仍由Swoole底层管理,只是失去引用,变成“幽灵连接” - 正确姿势:在
onWorkerStart中创建连接池(如swoole/coroutine-pool),所有协程统一从池中get()和put();或者直接复用一个全局Co\Redis实例(需确保线程/协程安全,通常pool更稳妥)
真正难处理的不是“要不要复用”,而是复用后如何隔离状态——比如Redis事务、SELECT db、连接超时设置等上下文,必须靠连接池的“借用-归还”契约来保证,而不是靠对象实例本身。










