了解原子变量和操作

时间:2013-06-27 14:19:02

标签: c++ multithreading atomic atomicity memory-fences

我一遍又一遍地阅读了boost和std(c ++ 11)原子类型和操作,但我仍然不确定我是否理解它(在某些情况下我根本不理解它)。所以,我有几个问题。

我用来学习的资料来源:


请考虑以下代码段:

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:以下陈述是真的吗?

Line * 1确保当在此行下完成的操作(例如* 2)可见时(对于使用acquire的其他线程),* 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:以下陈述是否属实?

  • Line * 3确保如果发布顺序下的某些操作(在其他线程中,如* 2)可见,则发布顺序上方的每个操作(例如* 1)也将可见。
  • 这意味着* 5处的断言永远不会失败(默认值为false)。
  • 但是这并不能保证即使物理上(处理器中)* 2发生在* 3之前,它也会被上面的剪断(在不同的线程中运行)可见 - 函数read_y_then_x()仍然可以读取旧值。只有确定的是,如果y为真,x也将为真。

#5:向原子整数递增(加1的操作)可以是memory_order_relaxed,并且不会丢失数据。唯一的问题是结果可见性的顺序和时间。


根据提升,以下剪辑是工作参考计数器:

#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为什么在参考计数器达到零后有获取订单?我们刚刚读到计数器为零,并且没有使用其他原子变量(指针本身没有标记/使用)。

3 个答案:

答案 0 :(得分:1)

1:否。释放围栏与所有获取操作和围栏同步。如果在第三个线程中有第三个atomic<bool> z被操纵,那么栅栏也会与第三个线程同步,这是不必要的。话虽如此,他们将在x86上采取相同的行动,但那是因为x86具有非常强的同步性。 1000核心系统使用的体系结构往往较弱。

2:是的,这是正确的。栅栏确保如果您看到后面的任何内容,您还会看到之前的所有内容。

3:一般来说它们是不同的,但实际上它们是相同的。允许编译器对不同变量上的两个松弛操作重新排序,但可能不会引入虚假操作。如果编译器有任何方式确信它需要读取x,那么在读取y之前可能会这样做。在您的特定情况下,这对于编译器来说非常困难,但是在许多类似的情况下,这种重新排序是公平的游戏。

4:所有这些都是真的。原子操作保证了一致性。他们并不总能保证事情按照你想要的顺序发生,他们只是防止破坏你的算法的病态命令。

5:正确。轻松的操作确实是原子的。他们只是不同步任何额外的内存

6:对于任何给定的原子对象M,C ++保证在M上有一个“官方”命令。你没有看到M的“最新”值,就像C ++和处理器一样,所有线程都会看到M的一系列一致值。如果两个线程递增引用计数,然后递减它,则没有保证将其减少为0,但是有一个保证,其中一个将看到它将它递减为0.它们都没有办法看到他们减少了2&gt; 1和2-> 1,但不知何故,refcount将它们组合为0.一个线程将始终看到2-> 1而另一个将看到1-> 0。

请记住,内存顺序更多的是围绕原子同步内存。无论你使用什么内存顺序,都可以正确处理原子。

7:这个比较棘手。 7的简短版本是递减是释放顺序,因为某些线程将必须运行x的析构函数,并且我们希望确保它在所有线程上看到x上的所有操作。在析构函数上使用释放顺序可满足此需求,因为您可以证明它是有效的。负责删除x的人在此之前获取所有更改(使用栅栏确保删除器中的原子不会向上漂移)。在线程释放自己的引用的所有情况下,显然所有线程在调用删除器之前将具有释放顺序减少。如果一个线程递增引用计数而另一个线程递减它,则可以证明唯一有效的方法是线程是否相互同步,以便析构函数看到两个线程的结果。无论如何都无法同步会产生竞争案例,因此用户有义务做到正确。

答案 1 :(得分:0)

1

在思考#1后,我确信§29.8.3 [atomics.fences]中的这个论点{{1}}并不等同于:{/ p>

  

释放栏A与对原子执行获取操作的原子操作B同步   对象M如果存在原子操作X使得A在X之前被排序,则X修改M和B   读取由X写入的值或由假设释放序列X中的任何副作用写入的值   如果它是一个释放操作,它将会结束。

本段说明发布 fence 只能与获取操作同步。但是发布操作可以另外与使用操作同步。

答案 2 :(得分:0)

带有获取栅栏的void read_y_then_x()将栅栏放在错误的位置。它应该放在两个原子载荷之间。获取围栏基本上使得围栏上方的所有负载都有点像获取负载,除非在执行围栏之前不会建立之前发生的事件。