fetch_sub 真的是原子的吗?

Is fetch_sub really atomic?

我有以下代码(用 C++ 编写):

StringRef 中的代码 class:

    inline void retain() const {
        m_refCount.fetch_add(1, std::memory_order_relaxed);
    }
    inline void release() const {
        if(m_refCount.fetch_sub(1, std::memory_order_release) == 1){
            std::atomic_thread_fence(std::memory_order_acquire);
            deleteFromParent();
        }
    }

InternedString 中的代码:

public:
    inline InternedString(){
        m_ref = nullptr;
    }
    inline InternedString(const InternedString& other){
        m_ref = other.m_ref;
        if(m_ref)
            m_ref->retain();
    }
    inline InternedString(InternedString&& other){
        m_ref = other.m_ref;
        other.m_ref = nullptr;
    }
    inline InternedString& operator=(const InternedString& other){
        if(&other == this)
            return *this;
        if(other.m_ref)
            other.m_ref->retain();
        if(m_ref)
            m_ref->release();
        m_ref = other.m_ref;
        return *this;
    }
    inline InternedString& operator=(InternedString&& other){
        if(&other == this)
            return *this;
        if(m_ref)
            m_ref->release();
        m_ref = other.m_ref;
        other.m_ref = nullptr;
        return *this;
    }
    /*! @group Destructors */
    inline ~InternedString(){
        if(m_ref)
            m_ref->release();
    }
private:
    inline InternedString(const StringRef* ref){
        assert(ref);
        m_ref = ref;
        m_ref->retain();
    }

当我在多个线程中执行此代码时,为同一对象多次调用 deleteFromParent()。我不明白为什么......即使我过度释放我仍然不应该得到这种行为,我想......

有人可以帮助我吗?我做错了什么?

fetch_sub 尽可能原子化,但这不是问题所在。

尝试像这样修改您的代码:

    if(m_refCount.fetch_sub(1, std::memory_order_release) == 1){
        Sleep(10);
        std::atomic_thread_fence(std::memory_order_acquire);
        deleteFromParent();

看看会发生什么。

如果您的析构函数被使用您的 InternedString 运算符的线程抢占,它们将很高兴地在不知不觉中获得对即将被删除的对象的引用。
这意味着您的其余代码可以自由引用已删除的对象,从而导致各种 UB,包括可能重新增加您的完美原子引用计数,从而导致多个完美原子破坏。

假设任何人都可以在不首先锁定析构函数的情况下复制引用,这是完全错误的,而且如果你将它埋在教科书之下,只会变得更糟,因为需要一连串的运算符来隐藏最终用户的引用杂耍。

如果任何任务都可以随时删除您的对象,像 InternedString a = b; 这样的代码将无法知道 b 是否是有效对象。
仅当在对象确实有效的时间设置了所有引用时,引用计数机制才会按预期工作。
您可以做的是在代码段中创建尽可能多的 InternedStrings ,在这些代码段中不能并行发生删除(无论是在初始化期间还是通过普通互斥锁),但是一旦析构函数松散,就可以参考杂耍了。

在不使用互斥锁或其他同步对象的情况下实现该功能的唯一方法是添加一种获取引用的机制,让用户知道该对象已被删除。这里是an example of how that could be done

现在,如果您尝试将其全部隐藏在五个运算符规则的地毯下,唯一剩下的解决方案是将某种 valid 属性添加到您的 InternedString,每一位在尝试访问底层字符串之前必须检查代码。

这相当于将多任务处理问题丢给界面最终用户的桌面,他们在最好的情况下最终会使用互斥体来防止其他代码从他脚下删除对象,或者可能只是修补与代码直到隐式同步显然解决了这个问题,在应用程序中植入了如此多的滴答作响的定时炸弹。

原子计数器and/or 结构不能替代多任务同步。除了一些可以设计超智能算法的专家外,原子变量只是一个包裹着大量语法糖的巨大陷阱。