支持从销毁中对象构造的原子侵入式共享指针实现探讨
侵入式共享指针的并发拷贝确定性问题
侵入式共享指针相比带独立控制块的经典共享指针有个优势——它支持单写多读场景下的原子无锁并发操作,但存在一个核心限制:当指针被重置且引用计数降至0、缓冲区被销毁时,此时对它进行并发拷贝无法得到确定的返回结果(预期应该是要么拿到引用计数正确的旧有效指针,要么拿到合法的空指针)。有没有办法消除这个限制?
下面是一个无效的实现方案:
template<typename T> class TIntrusiveSharedPointer { public: TIntrusiveSharedPointer() { AP.store(nullptr); } ~TIntrusiveSharedPointer() { Reset(); } // Resets the reference void Reset() noexcept { T* OldP = AP.exchange(nullptr, std::memory_order_acq_rel); if(OldP && OldP->RefCount.fetch_sub(1, std::memory_order_acq_rel)==1) { delete OldP; } } /* Copy constructor */ TIntrusiveSharedPointer(const TIntrusiveSharedPointer& Other) { while(true) { T* NewP = Other.AP.load(std::memory_order_consume); if(!NewP) { AP.store(nullptr, std::memory_order_release); return; } //We cannot safely read RefCount here, because even if we loaded //A non zero NewP, RefCount might already have been decremented to 0 //And pointer destroyed if(uint64_t Count = NewP->RefCount.load(std::memory_order_acquire)) { //Current RefCount is >=1, we can increment if(NewP->RefCount.compare_exchange_weak ( Count, Count+1, std::memory_order_acquire)) { AP.store(NewP, std::memory_order_release); return; } } else { //Current RefCount is zero, buffer about to be destroyed AP.store(nullptr, std::memory_order_release); return; } } } private: std::atomic<T*> AP; }; TIntrusiveSharedPointer<SomeClass> A; void Thread1() { A.Reset(); } void Thread2() { //Undefined behavior if run concurrently and A buffer is destroyed TIntrusiveSharedPointer<SomeClass> B(A); }
无效实现的核心问题
这个实现的拷贝构造逻辑存在致命竞态:当线程2加载到Other.AP的非空指针后,线程1可能已经完成Reset——把原子指针置空、引用计数减到0并销毁了对象。此时线程2再访问NewP->RefCount就会触发未定义行为,因为对象内存已经被释放。
可行解决方案
要解决这个问题,核心是确保对象销毁前,所有并发拷贝操作要么成功拿到有效引用,要么观察到空指针,不会出现访问已销毁内存的情况。下面是两种实用方案:
方案1:侵入式双引用计数(强+弱)
让被管理对象同时维护强、弱两个原子计数:
- 强引用计数:控制对象的可用性,只有强引用>0时,对象才能被正常访问;
- 弱引用计数:控制对象内存的释放时机,只有当强、弱引用都为0时,才真正销毁对象;
- 拷贝共享指针时,先尝试增加强引用计数,若失败(强引用已为0),则等待原子指针置空后返回空;
- Reset操作时,先置空原子指针,再减少强引用计数;只有强引用为0后,再检查弱引用,确认无残留访问后销毁对象。
简化实现示例
#include <atomic> template<typename T> class IntrusiveSharedPtr { public: IntrusiveSharedPtr() : ptr_(nullptr) {} ~IntrusiveSharedPtr() { Reset(); } // 拷贝构造 IntrusiveSharedPtr(const IntrusiveSharedPtr& other) { while (true) { T* p = other.ptr_.load(std::memory_order_acquire); if (!p) { ptr_.store(nullptr, std::memory_order_release); return; } // 尝试增加强引用,成功则持有指针 if (p->strong_ref.fetch_add(1, std::memory_order_acq_rel) > 0) { ptr_.store(p, std::memory_order_release); return; } // 强引用已为0,对象正在销毁,自旋等待指针置空 std::atomic_thread_fence(std::memory_order_acquire); } } void Reset() noexcept { T* old_ptr = ptr_.exchange(nullptr, std::memory_order_acq_rel); if (old_ptr) { // 强引用减1 if (old_ptr->strong_ref.fetch_sub(1, std::memory_order_acq_rel) == 1) { // 强引用为0,检查弱引用 if (old_ptr->weak_ref.fetch_sub(1, std::memory_order_acq_rel) == 1) { delete old_ptr; } } } } // 补充:移动构造、赋值重载等操作省略 private: std::atomic<T*> ptr_; }; // 被管理对象必须包含强/弱引用计数 class SomeClass { public: std::atomic<size_t> strong_ref = 1; // 初始强引用为1(对象创建时持有) std::atomic<size_t> weak_ref = 1; // 初始弱引用为1 // ... 其他业务成员 };
方案2:基于危险指针(Hazard Pointers)的内存回收
如果不想引入双计数,可以用危险指针机制:
- 拷贝共享指针时,先把当前指针注册为"危险指针",确保对象不会在访问期间被销毁;
- Reset操作时,检查是否有危险指针指向该对象,若有则延迟销毁,直到危险指针被解除注册;
- 这种方式实现复杂度较高,但不需要修改被管理对象的结构。
总结
最实用的方案是侵入式双引用计数,它逻辑清晰、实现难度适中,能完美解决并发拷贝的确定性问题,同时保留侵入式共享指针的无锁并发优势。
内容的提问来源于stack exchange,提问作者galinette
相关产品推荐
相关产品推荐

