对象池应优先用 std::unique_ptr + 自定义 deleter;需对齐内存防 false sharing;acquire 失败须处理 fallback;线程安全仅限单次调用,语义与内存生命周期必须明确分离。

对象池该用 std::shared_ptr 还是裸指针?
裸指针最轻量,但极易 double-free 或提前释放;std::shared_ptr 自动管理生命周期,但引入引用计数开销和原子操作,反而抵消池的性能收益。真实场景中,90% 的高性能对象池都用 std::unique_ptr + 自定义 deleter,既避免共享开销,又防止误用。
- 自定义 deleter 把归还逻辑写死:
[](MyClass* p) { ObjectPool::instance().release(p); } - 构造
std::unique_ptr时必须传这个 deleter,否则归还不生效 - 不要把池中对象的指针存成裸指针长期持有——哪怕只存一个 tick,也建议用
std::unique_ptr包一层
预分配内存怎么避免 false sharing?
多个线程频繁申请/释放同一 cache line 里的对象(比如连续 new 出来的 8 个 MyClass),会导致 cache line 在核间反复同步,性能暴跌。这不是理论问题,实测在 4 核以上机器上吞吐可能掉 30%+
- 对象大小向上对齐到 64 字节(常见 cache line 长度):
alignas(64) struct MyClass { ... }; - 预分配数组别用
new MyClass[n],改用operator new(n * sizeof(MyClass))+ placement new,确保每块内存起始地址对齐 - 如果池支持多类型,不同类别的对象池内存必须物理隔离,不能混在一块大 buffer 里
ObjectPool::acquire() 返回空指针时怎么办?
不是所有对象池都默认扩容。很多“高性能”实现会直接返回 nullptr,尤其在线程局部池(TLS pool)场景下。这时业务代码不能假设“池永远够用”,必须处理失败路径。
- 先检查是否允许动态扩容:有些池提供
acquire_or_fail()和acquire_or_grow()两个接口,别混用 - 若不允许扩容,常见做法是 fallback 到
new,但要记录日志:if (!p) { log_warn("pool exhausted, falling back to heap"); p = new MyClass; } - 注意:fallback 对象不能丢进池的
release(),否则触发未定义行为——池只认自己acquire()出去的对象
线程安全边界在哪?
多数对象池只保证 acquire()/release() 单次调用的原子性,不保证跨多次调用的顺序一致性。比如线程 A acquire 后还没初始化完,线程 B 就 release 了同一地址,就炸了。
立即学习“C++免费学习笔记(深入)”;
- 池本身不解决对象内部状态竞争——初始化、使用、销毁的临界区得业务自己加锁或用无锁结构
- TLS 池(每个线程独享子池)能避开大部分锁,但要注意首次访问 TLS 变量有开销,且子池空闲内存无法被其他线程复用
- 如果用全局锁,别锁整个
acquire()流程——把内存分配(lock)和对象构造(unlock 后)拆开,否则构造函数耗时会拖慢所有线程
对象池真正的坑不在分配逻辑,而在对象的“语义生命周期”和“内存生命周期”的错位。一个 MyClass 实例从池里出来,谁负责调它的构造函数?谁决定它什么时候算“用完”?这些约定一旦模糊,调试时连 core dump 都看不出是池的问题。











