我一遍又一遍地阅读关于 boost 和 std 的 (c++11) 原子类型和操作,但我仍然不确定我是否理解正确(在某些情况下我根本不理解)。所以,我有几个问题。
我用于学习的资源:
- 提升文档:http://www.boost.org/doc/libs/1_53_0/doc/html/atomic.html
- http://www.developerfusion.com/article/138018/memory-ordering-for-atomic-operations-in-c0x/
考虑以下代码段:
atomic<bool> x,y;
void write_x_then_y()
{
x.store(true, memory_order_relaxed);
y.store(true, memory_order_release);
}
#1:它等同于下一个吗?
atomic<bool> x,y;
void write_x_then_y()
{
x.store(true, memory_order_relaxed);
atomic_thread_fence(memory_order_release); // *1
y.store(true, memory_order_relaxed); // *2
}
#2:以下陈述是否正确?
*1 行确保,当在该行下完成的操作(例如 *2)可见时(对于使用获取的其他线程),*1 之上的代码也将可见(具有新值)。
下一个片段扩展了上面的片段:
void read_y_then_x()
{
if(y.load(memory_order_acquire))
{
assert(x.load(memory_order_relaxed));
}
}
#3:它等同于下一个吗?
void read_y_then_x()
{
atomic_thread_fence(memory_order_acquire); // *3
if(y.load(memory_order_relaxed)) // *4
{
assert(x.load(memory_order_relaxed)); // *5
}
}
#4:以下陈述是否正确?
- 第 *3 行确保如果释放顺序下的某些操作(在其他线程中,如 *2)可见,则释放顺序之上的每个操作(例如 *1)也将可见。
- 这意味着 assert at *5 永远不会失败(默认值为 false)。
- 但这并不能保证即使物理上(在处理器中)*2 发生在 *3 之前,它也会通过上面的片段可见(在不同的线程中运行)——函数 read_y_then_x() 仍然可以读取旧值。唯一可以确定的是,如果 y 为真,则 x 也为真。
#5:原子整数的递增(加1操作)可以是memory_order_relaxed并且没有数据丢失。唯一的问题是结果可见的顺序和时间。
根据 boost,以下片段是工作引用计数器:
#include <boost/intrusive_ptr.hpp>
#include <boost/atomic.hpp>
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;
}
}
};
#6 为什么要减少使用的 memory_order_release?它是如何工作的(在上下文中)?如果我之前写的是真的,是什么让返回值成为最新值,尤其是当我们在读取之后而不是之前/期间使用获取时?
#7 为什么引用计数器归零后会有acquire order?我们只是读到计数器为零并且没有使用其他原子变量(指针本身没有被标记/使用)。
最佳答案
1:否。释放栅栏与所有获取操作和栅栏同步。如果有第三个atomic<bool> z
在第三个线程中被操作,栅栏也会与第三个线程同步,这是不必要的。也就是说,它们在 x86 上的行为相同,但那是因为 x86 具有非常强的同步性。 1000 核心系统上使用的架构往往更弱。
2:是的,这是正确的。栅栏确保如果您看到后面的任何内容,您也会看到前面的所有内容。
3:一般来说它们是不同的,但实际上它们是相同的。允许编译器对不同变量的两个宽松操作重新排序,但不得引入虚假操作。如果编译器有任何方法确信它需要读取 x,它可能会在读取 y 之前这样做。在您的特定情况下,这对编译器来说非常困难,但在许多类似的情况下,这种重新排序是公平的游戏。
4:这些都是真的。原子操作保证了一致性。它们并不总是保证事情按照您想要的顺序发生,它们只是防止破坏您的算法的病态顺序。
5:正确。宽松的操作是真正原子的。他们只是不同步任何额外的内存
6:对于任何给定的原子对象 M
, C++ 保证对 M
的操作有一个“官方”顺序.您看不到 M
的“最新”值与 C++ 和处理器一样,所有线程都将看到一系列一致的 M
值。 .如果两个线程递增引用计数,然后递减它,则无法保证哪个线程会将其递减为 0,但可以保证其中一个线程会看到它已将其递减为 0。它们都无法做到看到它们递减 2->1 和 2->1,但不知何故,refcount 将它们合并为 0。一个线程将始终看到 2->1,而另一个将看到 1->0。
请记住,内存顺序更多的是围绕原子同步内存。无论您使用什么内存顺序,原子都会得到正确处理。
7:这个比较棘手。 7 的简短版本是递减是释放顺序,因为某些线程将不得不运行 x 的析构函数,我们希望确保它看到所有线程上对 x 进行的所有操作。在析构函数上使用释放顺序可以满足此需求,因为您可以证明它有效。负责删除 x 的人在删除之前获取所有更改(使用栅栏确保删除器中的原子不会向上漂移)。在线程释放它们自己的引用的所有情况下,很明显所有线程在调用删除器之前都会有一个释放顺序递减。在一个线程递增引用计数而另一个线程递减它的情况下,您可以证明这样做的唯一有效方法是线程彼此同步,以便析构函数看到两个线程的结果。无论如何,同步失败都会产生竞争情况,因此用户有义务正确处理。
关于c++ - 了解原子变量和操作,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/17345680/