2

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

StringRef 类中的代码:

    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() 会为同一个对象多次调用。我不明白为什么......即使我过度释放我仍然不应该得到这种行为,我想......

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

4

1 回答 1

0

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是有效对象。
只有在对象确实有效时设置了所有引用,引用计数机制才会按预期工作。
您可以做的是在代码部分创建任意数量InternedString的 s,在这些代码部分中不能并行发生删除(无论是在 init 期间还是通过普通互斥锁),但是一旦析构函数处于松散状态,那就是参考杂耍。

不使用互斥锁或其他同步对象的唯一方法是添加一种获取引用的机制,让用户知道该对象已被删除。这是一个如何做到这一点的例子

现在,如果您尝试将其全部隐藏在五个运算符的规则下,唯一剩下的解决方案是向validInternedString的 .

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

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

于 2015-02-22T10:11:26.690 回答