c++ - fetch_sub 真的是原子的吗?

标签 c++ multithreading reference-counting stdatomic

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

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

最佳答案

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 .

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

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

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

关于c++ - fetch_sub 真的是原子的吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/28652236/

相关文章:

c++ - 不可用的 Ui 属性

c++ - 编译器对结构重新排序

c++ - 声明为 void 的变量或字段(函数)

C++ weak_ptr创建性能

c++ - C++ OO 风格的引用计数

c++ - Qt - 对...的 undefined reference (Ubuntu 14.04 LTS 64 位)

java - 如何修复 'Offset commit failed on partition com.application.iot.measure.stage-0 at offset 1053078427: The request timed out.'

java - 在线程中获得特定输入后,如何退出线程中运行的循环?在java中

c# - Thread.Sleep(1) 的目的?

.net - 如何获取托管对象的引用计数?