0

我一遍又一遍地阅读了关于 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:以下陈述是真的吗?

*1 行保证,当在此行下完成的操作(例如 *2)是可见的(对于使用获取的其他线程),*1 以上的代码也将是可见的(具有新值)。


Next snipped 扩展了上面的那些:

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)也将可见。
  • 这意味着 *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 为什么引用计数器归零后有获取命令?我们刚刚读到计数器为零并且没有使用其他原子变量(指针本身没有被标记/使用)。

4

3 回答 3

1

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

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

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

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

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

6:对于任何给定的原子对象M,C++ 保证对M. 你看不到MC++ 的“最新”值,处理器保证所有线程都会看到一系列一致的M. 如果两个线程递增 refcount,然后递减它,则无法保证哪个线程会将其递减为 0,但可以保证其中一个线程将看到它将其递减为 0。他们没有办法看到它们递减了 2->1 和 2->1,但是 refcount 以某种方式将它们组合为 0。一个线程将始终看到 2->1,而另一个线程将看到 1->0。

请记住,内存顺序更多的是关于围绕原子同步内存。无论您使用什么内存顺序,原子都会得到正确处理。

7:这个比较棘手 7 的简短版本是递减是发布顺序,因为某些线程将不得不运行 x 的析构函数,并且我们希望确保它看到所有线程上对 x 进行的所有操作。在析构函数上使用发布顺序可以满足这种需求,因为您可以证明它有效。负责删除 x 的人会在删除之前获取所有更改(使用栅栏确保删除器中的原子不会向上漂移)。在所有线程释放它们自己的引用的情况下,很明显所有线程在删除器被调用之前都会有一个释放顺序递减。在一个线程增加引用计数而另一个减少引用计数的情况下,您可以证明这样做的唯一有效方法是线程彼此同步,以便析构函数看到两个线程的结果。

于 2013-09-12T06:24:42.050 回答
0

1

在思考了#1之后,我确信它们与以下论点不§29.8.3等价[atomics.fences]

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

这一段说释放栅栏只能与获取操作同步。但是释放操作可以另外与消费操作同步。

于 2013-07-24T19:25:41.237 回答
0

带有获取栅栏的 void read_y_then_x() 栅栏在错误的位置。它应该放在两个原子负载之间。获取栅栏本质上使栅栏上方的所有负载的行为有点像获取负载,除了在执行栅栏之前不会建立之前发生的情况。

于 2013-09-12T19:27:06.703 回答