C++如何实现基于引用计数的跨线程对象生命周期安全管理?(内存安全)

3次阅读

std::shared_ptr自身线程安全,但所指对象访问需额外同步;weak_ptr需在lock成功后立即使用shared_ptr访问对象,避免use-after-free;make_shared优化内存分配与缓存局部性。

C++如何实现基于引用计数的跨线程对象生命周期安全管理?(内存安全)

std::shared_ptr 在多线程中直接共享是否安全?

是安全的,但仅限于 std::shared_ptr 对象本身的拷贝/赋值/析构操作 —— 这些内部引用计数增减是原子的。真正不安全的是通过它访问所指向的对象(get()*ptr)时没有额外同步。

常见错误现象:std::shared_ptr 本身没崩溃,但业务逻辑读写对象成员时出现数据竞争、野指针或断言失败。

  • 引用计数原子操作 ≠ 所指对象线程安全
  • 多个线程同时调用 ptr->do_something(),而 do_something()const 且修改成员变量 → 必须加锁或用 std::atomic 等手段保护对象状态
  • 若只读访问,且对象构造完成后不再修改(即“不可变对象”),则无需额外同步

如何避免 shared_ptr 析构时的跨线程 use-after-free?

核心在于:最后一个 std::shared_ptr 的析构可能发生在任意线程,而该析构会触发 delete(或自定义 deleter),如果此时其他线程还在用原始指针或 weak_ptr 锁定失败后继续访问,就崩了。

使用场景:工作线程持有一个 std::weak_ptr 定期尝试处理对象,主线程随时可能释放对象。

立即学习C++免费学习笔记(深入)”;

  • 永远不要从 std::weak_ptr::lock() 得到 std::shared_ptr 后,再解引用裸指针(如 ptr.get())并长期持有
  • 所有对对象的访问,必须包裹在 if (auto sp = wp.lock()) { /* 用 sp->xxx */ } 内,且不在 if 块外保留 sp.get()
  • 避免在 deleter 中执行耗时或依赖其他线程资源的操作(比如发消息、等锁),否则可能阻塞析构线程并引发死锁

为什么不能用裸 new + 自己写引用计数替代 shared_ptr?

自己实现跨线程安全的引用计数,难点不在“计数”,而在“内存序”和“析构时机”的精确控制。标准库的 std::shared_ptrc++11 起已明确定义了引用计数操作的 memory_order_acq_rel 行为,且与 std::weak_ptr 协同处理 ABA 和竞态析构问题。

容易踩的坑:

  • std::atomic_int 做计数器,但忘记在递减后、delete 前插入 memory_order_acquire 栅栏 → 其他线程可能看到部分构造的对象状态
  • 在 deleter 中 delete 对象,但没确保该 delete 不会触发另一个线程正在执行的虚函数调用(尤其涉及多态和动态加载模块时)
  • 忽略 std::shared_ptr 的控制块(control block)分配策略:默认分配,若高频创建销毁,会成为性能瓶颈;可重载 new 或用 make_shared 减少一次分配

std::make_shared 比 new + shared_ptr 构造快在哪?

关键差异在内存布局:std::make_shared 把控制块和对象数据一次性分配在同一块内存里;而 std::shared_ptr<t>(new T)</t> 是两次独立分配 —— 控制块一次、对象一次。

性能影响:

  • 减少一次 malloc 调用,降低内存碎片和分配开销
  • 提升缓存局部性:引用计数和对象数据更可能落在同一 cache line
  • 但注意:若对象构造函数抛异常,make_shared 会同时释放控制块和未完成构造的对象;而手动 new + shared_ptr 构造时,若 new T 成功但 shared_ptr 构造失败(如控制块分配失败),会造成内存泄漏(极罕见,但需知道)

事情说清了就结束。最常被忽略的不是怎么写引用计数,而是忘了“引用计数安全”和“对象数据安全”是两层事,中间那条线划在哪,得看具体访问模式。

text=ZqhQzanResources