在没有互斥锁的情况下重新计数时如何避免竞争条件?
How to avoid race condition when refcounting without a mutex?
我试图找出如何避免以下代码中的竞争条件,线程 A 获取数据块,然后线程 B Releasing/deleting 它,然后线程 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();
}
};
I think that it's possible to fix this with atomic_thread_fence
atomic_thread_fence
仅在您使用比默认值 seq_cst
更弱的内存排序时才有用(有关栅栏和内存排序的更多信息,请参阅 Jeff Preshing's article about C++11 fences。Jeff Preshing 的文章非常出色;当你试图理解无锁编程时,一定要阅读其中的大部分内容)。
atomic_thread_fence
只能限制当前线程的内存操作如何变得全局可见的重新排序。它本身并不等待其他线程中的某些东西。
当您尝试添加引用时,请准备好发现它已经降为零。即 AddRef()
可能会失败,如果你来不及,并且另一个线程已经开始销毁被引用的对象。
所以 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 如果旧值太高则执行它低的。如果 two 线程同时递增,后者将需要额外的工作来避免竞争条件。 (第二个线程会看到 old_count 为 1 并认为没问题。)您可以通过让 Release
函数将引用计数设置为一个大的负数 来解决这个问题 开始销毁一个块,但这很容易验证,而且几乎总是在第一次尝试时成功的 CAS 几乎不比实际的 fetch_add
慢。与 CAS 相比,单独的原子加载几乎是免费的,尤其是在 x86 上。 (您也可以使用 memory_order_relaxed
使其在 weakly-ordered 架构上也成为 near-free。)
请注意,当引用计数达到零时,您的引用计数不能成为您delete
的数据块的一部分。如果你这样做了,调用 get_datablock
并执行 m_datablock.load()
,然后休眠,然后使用 datablock->AddRef()
取消引用该指针的线程可能会出现段错误(或导致其他未定义的行为),如果 pointed-to 内存在休眠时被另一个线程删除了。
这个答案没有解决 整个 问题(管理引用计数块,同时仍然允许 set_datablock
API 中的 exchange
]。我不确定 API 设计是否真的有效。
它也不是一个完整的工作 atomic_shared_pointer
实现。
如果您想知道它是如何工作的,请查看它的文档,或者希望有人写了一篇关于它如何实现的 post。 Open-source 它的库实现存在,但可能很难阅读。
我试图找出如何避免以下代码中的竞争条件,线程 A 获取数据块,然后线程 B Releasing/deleting 它,然后线程 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();
}
};
I think that it's possible to fix this with atomic_thread_fence
atomic_thread_fence
仅在您使用比默认值 seq_cst
更弱的内存排序时才有用(有关栅栏和内存排序的更多信息,请参阅 Jeff Preshing's article about C++11 fences。Jeff Preshing 的文章非常出色;当你试图理解无锁编程时,一定要阅读其中的大部分内容)。
atomic_thread_fence
只能限制当前线程的内存操作如何变得全局可见的重新排序。它本身并不等待其他线程中的某些东西。
当您尝试添加引用时,请准备好发现它已经降为零。即 AddRef()
可能会失败,如果你来不及,并且另一个线程已经开始销毁被引用的对象。
所以 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 如果旧值太高则执行它低的。如果 two 线程同时递增,后者将需要额外的工作来避免竞争条件。 (第二个线程会看到 old_count 为 1 并认为没问题。)您可以通过让 Release
函数将引用计数设置为一个大的负数 来解决这个问题 开始销毁一个块,但这很容易验证,而且几乎总是在第一次尝试时成功的 CAS 几乎不比实际的 fetch_add
慢。与 CAS 相比,单独的原子加载几乎是免费的,尤其是在 x86 上。 (您也可以使用 memory_order_relaxed
使其在 weakly-ordered 架构上也成为 near-free。)
请注意,当引用计数达到零时,您的引用计数不能成为您delete
的数据块的一部分。如果你这样做了,调用 get_datablock
并执行 m_datablock.load()
,然后休眠,然后使用 datablock->AddRef()
取消引用该指针的线程可能会出现段错误(或导致其他未定义的行为),如果 pointed-to 内存在休眠时被另一个线程删除了。
这个答案没有解决 整个 问题(管理引用计数块,同时仍然允许 set_datablock
API 中的 exchange
]。我不确定 API 设计是否真的有效。
它也不是一个完整的工作 atomic_shared_pointer
实现。
如果您想知道它是如何工作的,请查看它的文档,或者希望有人写了一篇关于它如何实现的 post。 Open-source 它的库实现存在,但可能很难阅读。