【发布时间】:2015-03-01 07:23:55
【问题描述】:
Boost 提供了一个sample atomically reference counted shared pointer
这里是相关的代码sn-p以及对使用的各种排序的解释:
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 操作,但这会导致不必要的“获取”操作,当 参考计数器尚未达到零,可能会施加性能 罚款。
我不明白为什么在delete x 操作之前需要memory_order_acquire 屏障。具体来说,编译器/处理器如何在不违反单线程语义的情况下,在fetch_sub 之前重新排序delete x 的内存操作以及对x == 1 的值的测试是安全的?
编辑我想,我的问题不是很清楚。这是一个改写的版本:
读取 x (x->refcount_.fetch_sub(1, boost::memory_order_release) == 1) 和 delete x 操作之间的控制依赖关系是否提供任何排序保证?即使考虑单线程程序,编译器/处理器是否有可能在fetch_sub 和比较之前重新排序与delete x 操作相对应的指令?如果答案尽可能低级,并包含删除操作被重新排序(不影响单线程语义)的示例场景,从而说明保留顺序的必要性,那将非常有帮助。
【问题讨论】:
-
最好更抽象地考虑
acquire语义。 not (仅)关于是否对操作进行了重新排序,因此您关于重新排序的问题并不真正相关。这是关于这个线程是否“拉入”它的内存视图,在它自己的操作涉及这些内存位置之前,其他线程可能对引用计数以外的内存位置所做的任何更改。如果没有acquire,它就不会这样做,也就是说存在潜在的数据竞争。内存屏障不是(仅)对指令重新排序的限制,它们是git push和git pull。 -
@SteveJessop - 抽象视图对我帮助不大。 TBH 我发现它们比以硬件为中心的视图更令人困惑(和神奇)。例如,AFAIU 的获取和释放屏障并不关心其他线程的内存访问,而是简单地分别维护 load->[load/store] 和 store->[load/store] 的顺序,用于同一线程中的内存访问线。将它们视为
git push和git pull表明他们实际上知道图片中的其他线程,而他们显然不知道。 -
关于同一主题的另外两个 SO 问题:stackoverflow.com/questions/10268737/…、stackoverflow.com/questions/25304201/…。
-
我同意@SteveJessop 的观点,即抽象语义是推理这些事情的最佳方式,因为这是标准要求的。举一个具体的例子,将“释放”视为“刷新写入缓存”,将“获取”视为“刷新读取缓存”。想象一下,删除 x 的线程缓存了其他线程修改的部分对象。然后,如果没有“释放”和“获取”,
delete可能会在非 POD 对象上爆炸。 (对于 POD 对象,很难想象现实生活中的失败。但“它会触发未定义的行为”实际上是一个足够的答案......) -
@Nemo 所以你对 rel/acq 的描述是 rel 作用于以前的挂起写入,而 acq 作用于以前的记忆读取?
标签: c++ multithreading boost shared-memory atomic