重新排序和memory_order_relaxed

时间:2016-02-26 10:13:58

标签: c++ multithreading c++11 atomic memory-model

Cppreference为following example提供memory_order_relaxed

  

标记为memory_order_relaxed的原子操作不同步   操作,他们不订购记忆。他们只保证原子性   和修改顺序一致性。

然后解释说,xy最初为零,此示例代码

// Thread 1:
r1 = y.load(memory_order_relaxed); // A
x.store(r1, memory_order_relaxed); // B

// Thread 2:
r2 = x.load(memory_order_relaxed); // C 
y.store(42, memory_order_relaxed); // D

允许生成r1 == r2 == 42,因为:

  1. 虽然在线程1中A B之前排序,而在线程2中C 排序之前
  2. y的修改顺序中,没有任何内容可以阻止D出现在A之前,而在x的修改顺序中,B出现在C之前。
  3. 现在我的问题是:如果A和B不能在线程1中重新排序,并且类似地,线程2中的C和D(因为其中每个都是之前线程),不是第1点和第2点矛盾?换句话说,没有重新排序(如第1点似乎要求),第2点中的情景如何在下面显示,甚至可能?

    T1 ........... T2

    .............. D(y)

    A(y)的

    B(x)的

    .............. C(x)

    因为在这种情况下,C 在线程2中的 D之前排序,正如第1点所要求的那样。

4 个答案:

答案 0 :(得分:4)

  

没有重新排序(因为第1点似乎需要)

第1点并不意味着“没有重新排序”。它意味着在执行线程内对事件进行排序。编译器将在B之前发出A的CPU指令,在D之前发出C的CPU指令(尽管可能会被as-if规则破坏),但CPU没有义务按顺序执行它们,缓存/写入缓冲区/失效队列没有义务按顺序传播它们,并且内存没有义务统一。

(个别架构可能会提供这些保证)

答案 1 :(得分:1)

根据这篇帖子中的STR类比:C++11 introduced a standardized memory model. What does it mean? And how is it going to affect C++ programming?,我已经创建了一个可视化的可视化(如我所知),如下所示:

enter image description here

线程1首先看y=42,然后执行r1=y x=r1。线程2首先看到x=r1已经是42,然后它会执行r2=x,而 y=42

行表示各个线程的内存“视图”。这些行/视图无法跨越特定线程。但是,使用轻松的原子,一个线程的线/视图可以穿过其他线程。

修改

我想这与以下程序相同:

atomic<int> x{0}, y{0};

// thread 1:
x.store(1, memory_order_relaxed);
cout << x.load(memory_order_relaxed) << y.load(memory_order_relaxed);

// thread 2:
y.store(1, memory_order_relaxed);
cout << x.load(memory_order_relaxed) << y.load(memory_order_relaxed);

可以在输出上产生0110(SC原子操作不会发生这样的输出)。

答案 2 :(得分:0)

您对案文的解释是错误的。让我们打破这个:

  

标记为memory_order_relaxed的原子操作不是同步操作,它们不命令内存

这意味着这些操作不保证事件的顺序。正如原始文本中的该语句之前所解释的,允许多线程处理器在单个线程内重新排序操作。这可能会影响写入,读取或两者。此外,编译器允许在编译时执行相同的操作(主要用于优化目的)。要了解这与示例的关系,假设我们根本不使用atomic类型,但我们确实使用原子设计的原始类型(8位值...)。让我们重写一下这个例子:

// Somewhere...
uint8_t y, x;

// Thread 1:
uint8_t r1 = y; // A
x = r1; // B

// Thread 2:
uint8_t r2 = x; // C 
y = 42; // D

考虑到编译器和CPU都允许在每个线程中重新排序操作,很容易看出x == y == 42是如何可能的。

声明的下一部分是:

  

它们只保证原子性和修改顺序的一致性。

这意味着唯一的保证是每个操作都是原子的,也就是说,“中途通过”操作是不可能的。这意味着如果xatomic<someComplexType>,则一个线程无法将x视为在状态之间具有值。

应该已经清楚哪些地方有用了,但让我们来看一个具体的例子(仅用于演示,这不是你想要编码的方式):

class SomeComplexType {
  public:
    int size;
    int *values;
}

// Thread 1:
SomeComplexType r = x.load(memory_order_relaxed);
if(r.size > 3)
  r.values[2] = 123;

// Thread 2:
SomeComplexType a, b;
a.size = 10; a.values = new int[10];
b.size = 0; b.values = NULL;
x.store(a, memory_order_relaxed);
x.store(b, memory_order_relaxed);

atomic类型对我们的作用是保证线程1中的r不是状态之间的对象,具体而言,它是size&amp; values属性同步。

答案 3 :(得分:0)

专注于C ++内存模型(不是讨论编译器或硬件重新排序),导致r1 = r2 = 42的唯一执行是:

enter image description here

这里我用a替换了r1,用b替换了r2。 像往常一样,sb代表先前排序,并且只是线程间排序(指令在源代码中出现的顺序)。 rf是Read-From边缘,表示一端的读取/加载读取另一端写入/存储的值。

涉及sb和rf边缘的循环,如绿色突出显示,对于结果是必要的:y写在一个线程中,在另一个线程中读取到a和从那里写入x,读取在前一个线程中再次进入b(在写入y之前进行排序)。

有两个原因可能导致像这样的构造图形不可能:因果关系和因为rf读取隐藏的副作用。在这种情况下,后者是不可能的,因为我们只对每个变量写一次,所以很明显一次写入不能被另一次写入隐藏(覆盖)。

为了回答因果关系问题,我们遵循以下规则:当涉及单个存储器位置并且sb边缘的方向在循环中的任何位置(方向)时,不允许循环(不可能)在这种情况下,rf边缘不相关);或者,循环涉及多个变量,所有边(sb和rf)在同一方向上,并且AT MOST其中一个变量在不释放/获取的不同线程之间具有一个或多个rf边缘。

在这种情况下,循环存在,涉及两个变量(x的一个rf边缘和y的一个rf边缘),所有边缘处于相同的方向,但是两个变量具有松弛/松弛的rf边缘(即x和Y)。因此,没有因果关系违规,这是一个与C ++内存模型一致的执行。