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

How to avoid race condition when refcounting without a mutex?

本文关键字:新计数 何避免 竞争 条件 情况下      更新时间:2023-10-16

我正在尝试弄清楚如何避免以下代码中的竞争条件,线程 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();
}
};
我认为可以用

atomic_thread_fence来解决这个问题

仅当您使用的内存排序弱于默认seq_cst时,atomic_thread_fence才有用(有关围栏和内存排序的更多信息,请参阅 Jeff Preshing 关于 C++11 围栏的文章。杰夫·普雷辛的文章非常出色;当您尝试尝试无锁编程时,一定要阅读其中的大部分)。

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


当您尝试添加引用时,请准备好发现它已降至零。 即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,然后在旧值太低时取消执行。 后者需要额外的工作来避免两个线程同时递增时的争用情况。 (第二个线程会看到并old_count 1 并认为没问题。 您可以通过让Release函数在开始销毁块之前将 refcount 设置为大的负数来解决这个问题,但这很容易验证,并且几乎总是在第一次尝试时成功的 CAS 几乎不比实际fetch_add慢。 与 CAS 相比,单独的原子负载几乎是免费的,尤其是在 x86 上。 (您也可以使用memory_order_relaxed使其在弱序架构上接近自由。


请注意,您的引用计数不能是引用计数达到零时delete的数据块的一部分。 如果这样做,则调用get_datablock并执行m_datablock.load()的线程,然后休眠,然后取消引用该datablock->AddRef()指针,如果指向的内存在休眠时被另一个线程删除,则可能会出现段错误(或导致其他未定义的行为)。


这个答案并不能解决整个问题(在管理 refcount 块的同时仍然允许在set_datablockAPI 中进行exchange。 我不确定 API 设计是否真的有效。

它也不是一个完整的工作atomic_shared_pointer实现。

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