Boost提供sample atomically reference counted shared pointer
以下是相关的代码段以及所使用的各种排序的说明:
class X {
public:
typedef boost::intrusive_ptr<X> pointer;
X() : refcount_(0) {}
private:
mutable boost::atomic<int> refcount_;
friend void intrusive_ptr_add_ref(const X * x)
{
x->refcount_.fetch_add(1, boost::memory_order_relaxed);
}
friend void intrusive_ptr_release(const X * x)
{
if (x->refcount_.fetch_sub(1, boost::memory_order_release) == 1) {
boost::atomic_thread_fence(boost::memory_order_acquire);
delete x;
}
}
};
始终可以使用增加引用计数器 memory_order_relaxed:只能形成对对象的新引用 从现有引用,并从一个引用现有引用 线程到另一个必须已经提供任何所需的同步。
在一个对象中强制执行对对象的任何可能访问非常重要 线程(通过现有引用)在删除之前发生 对象在不同的线程中。这是通过“释放”实现的 删除引用后的操作(通过对对象的任何访问) 这个参考必须明显发生在之前),并且“获得” 删除对象之前的操作。
可以将memory_order_acq_rel用于fetch_sub 操作,但这导致不需要的“获取”操作时 参考计数器尚未达到零并且可能会产生性能 罚。
我无法理解为什么memory_order_acquire
屏障在delete x
操作之前是必要的。具体来说,编译器/处理器如何安全地在delete x
之前重新排序fetch_sub
的内存操作并在不违反单线程语义的情况下对x == 1
的值进行测试?
编辑我想,我的问题不是很清楚。这是一个改写版本:
读取x(x->refcount_.fetch_sub(1, boost::memory_order_release) == 1
)和delete x
操作之间的控制依赖性是否会提供任何排序保证?即使考虑单线程程序,编译器/处理器是否有可能在delete x
和比较之前对与fetch_sub
操作相对应的指令进行重新排序?如果答案尽可能低,并且包含一个示例场景,其中删除操作被重新排序(不影响单线程语义),从而说明需要保留排序,这将非常有用。
答案 0 :(得分:6)
考虑两个线程,每个线程持有一个对象的引用,这是最后两个引用:
------------------------------------------------------------
Thread 1 Thread 2
------------------------------------------------------------
// play with x here
fetch_sub(...)
fetch_sub(...)
// nothing
delete x;
当调用//play with x here
时,您必须确保线程2在delete x;
中对该对象所做的任何更改都是可见的。为此,您需要一个获取栅栏,它与memory_order_release
调用上的fetch_sub()
一起保证线程1所做的更改将可见。
答案 1 :(得分:2)
来自,http://en.cppreference.com/w/cpp/atomic/memory_order
memory_order_acquire - 具有此内存顺序的加载操作会对受影响的内存位置执行获取操作: 写入其他内存位置的线程执行了 释放在此主题中可见。
...
发布 - 获取订购
如果线程A中的原子存储被标记为std :: memory_order_release和 标记了来自同一变量的线程B中的原子加载 std :: memory_order_acquire,所有内存写入(非原子和放松 原子)发生在原子商店之前的观点 线程A,在线程B中成为可见的副作用,即一次 原子加载完成后,线程B保证看到一切 线程A写入内存。
仅在释放的线程之间建立同步 并获得相同的原子变量。其他线程可以看到 内存访问的顺序不同于其中一个或两个 同步线程。
在强烈排序的系统(x86,SPARC TSO,IBM大型机)上, 发布 - 获取订购对于大多数操作是自动的。 不会为此同步发出其他CPU指令 模式,只有某些编译器优化受到影响(例如 禁止编译器将非原子存储移动到原子之外 存储 - 释放或执行早于原子的非原子加载 负载获取)。在弱有序系统(ARM,Itanium,PowerPC)上, 必须使用特殊的CPU加载或内存栅栏指令。
这意味着 release 允许其他线程同步当前线程的挂起操作,而后面的 acquire 从其他线程获取所有修改后的更改。
在强烈有序的系统上,这并不重要。我不认为这些指令甚至会生成代码,因为CPU会在任何写入发生之前自动锁定缓存行。缓存保证一致。但是在每周订购的系统上,虽然原子操作定义得很好,但是可能存在对内存其他部分的待处理操作。
所以,让我们说线程A和B都共享一些数据D.
在删除之前获取线程栏,当前线程会同步其地址空间中其他线程的所有挂起操作。当删除发生时,它会看到A在#1中做了什么。
答案 2 :(得分:0)
我想我找到了一个相当简单的示例,说明了为什么需要获取围栏。
假设我们的X
如下所示:
struct X
{
~X() { free(data); }
void* data;
atomic<int> refcount;
};
让我们进一步假设我们有两个看起来像这样的函数foo
和bar
(我将内联引用计数递减):
void foo(X* x)
{
void* newData = generateNewData();
free(x->data);
x->data = newData;
if (x->refcount.fetch_sub(1, memory_order_release) == 1)
delete x;
}
void bar(X* x)
{
// Do something unrelated to x
if (x->refcount.fetch_sub(1, memory_order_release) == 1)
delete x;
}
delete
指令将执行x
的析构函数,然后释放x
占用的内存。让我们内联:
void bar(X* x)
{
// Do something unrelated to x
if (x->refcount.fetch_sub(1, memory_order_release) == 1)
{
free(x->data);
operator delete(x);
}
}
由于没有获取隔离,编译器可以决定在执行原子减量之前将地址x->data
加载到寄存器中(只要没有数据竞争,可观察到的效果将是相同的):
void bar(X* x)
{
register void* r1 = x->data;
// Do something unrelated to x
if (x->refcount.fetch_sub(1, memory_order_release) == 1)
{
free(r1);
operator delete(x);
}
}
现在,我们假设refcount
中的x
是2
,并且我们有两个线程。线程1调用foo
,线程2调用bar
:
x->data
加载到寄存器中。x->data
。refcount
从2
递减到1
。refcount
减少1
到0
。对我来说,关键的见解是“先前的写入在该线程中变得可见”可能意味着一些琐碎的事,例如“不要使用在隔离区之前缓存到寄存器的值”。