You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

支持从销毁中对象构造的原子侵入式共享指针实现探讨

侵入式共享指针的并发拷贝确定性问题

侵入式共享指针相比带独立控制块的经典共享指针有个优势——它支持单写多读场景下的原子无锁并发操作,但存在一个核心限制:当指针被重置且引用计数降至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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 20:35:05