在没有互斥锁的情况下引用计数时如何避免竞争条件?

时间:2018-02-24 22:30:19

标签: c++ atomic lock-free

我试图弄清楚如何避免以下代码中的竞争条件,线程A获取数据块,然后线程B释放/删除它,然后线程A AddRefing它。没有互斥锁可以解决这个问题吗?我认为用atomic_thread_fence解决这个问题是可能的,但我真的不知道它将如何应用于这种情况。

#include <atomic>

class Foo
{
    std::atomic<Datablock*> datablock

public:
    Datablock * get_datablock()
    {
        Datablock * datablock = m_datablock.load();
        if(datablock) datablock->AddRef();
        return datablock;
    }

    void set_datablock(Datablock* datablock)
    {
        datablock = m_datablock.exchange(datablock);
        if(datablock) datablock->Release();
    }
};

1 个答案:

答案 0 :(得分:2)

  

我认为可以用atomic_thread_fence

解决这个问题

atomic_thread_fence仅在使用弱于默认seq_cst的内存排序时才有用(有关栅栏和内存排序的更多信息,请参阅Jeff Preshing's article about C++11 fences。Jeff Preshing的文章非常好;绝对可以阅读当你想要无锁编程时,大部分都是这样的。

atomic_thread_fence只能限制当前线程的内存操作如何全局可见的重新排序。它本身并不等待其他线程中的某些东西。

当您尝试添加引用时,请准备好发现它已经降为零。 ie AddRef()可能会失败,如果你太晚了,另一个线程已经开始破坏refcounted对象。

因此AddRef的实现会像

那样
bool AddRef() {
    int old_count = m_refcount;

    do {
        if (old_count <= 0) {
            // we were too late; refcount had already dropped to zero
            // so another thread is already destroying the data block
            return false;
        }
    }while( !m_refcount.compare_exchange_weak(old_count, old_count+1) );

    return true;
}

我们使用CAS循环作为条件fetch_add而不是fetch_add然后 un 如果旧值太低则使用它。如果两个线程一次递增,后者将需要额外的工作来避免竞争条件。 (第二个线程会看到和old_count为1并且认为它没问题。)你可以通过让Release函数在开始之前将refcount设置为一个大的负数来解决这个问题。销毁一个块,但这很容易验证,并且在第一次尝试时几乎总是成功的CAS几乎不比实际的fetch_add慢。与CAS相比,单独的原子负载几乎是免费的,特别是在x86上。 (您可以使用memory_order_relaxed来使其在弱有序的体系结构上几乎免费。)

请注意,当引用次数达到零时,您的引用计数不能成为delete的数据块的一部分。如果你这样做,一个调用get_datablock然后执行m_datablock.load()的线程然后睡觉,然后取消引用带有datablock->AddRef()的指针可能会出现段错误(或引起其他未定义的行为)在睡着的时候被另一个线程删除了。

这个答案并不能解决整个问题(管理refcount块的同时仍然允许exchange API中的set_datablock。我不确定API设计确实有效。

这也不是一个完整的atomic_shared_pointer实施工作。

如果您想知道它是如何工作的,请查看其文档,或希望有人写一篇关于它如何实现的文章。它的开源库实现存在,但可能很难阅读。