Cppreference为following example提供memory_order_relaxed
:
标记为
memory_order_relaxed
的原子操作不同步 操作,他们不订购记忆。他们只保证原子性 和修改顺序一致性。
然后解释说,x
和y
最初为零,此示例代码
// 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
,因为:
y
的修改顺序中,没有任何内容可以阻止D出现在A之前,而在x
的修改顺序中,B出现在C之前。现在我的问题是:如果A和B不能在线程1中重新排序,并且类似地,线程2中的C和D(因为其中每个都是在之前线程),不是第1点和第2点矛盾?换句话说,没有重新排序(如第1点似乎要求),第2点中的情景如何在下面显示,甚至可能?
T1 ........... T2
.............. D(y)
A(y)的
B(x)的
.............. C(x)
因为在这种情况下,C 在线程2中的 D之前排序,正如第1点所要求的那样。
答案 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?,我已经创建了一个可视化的可视化(如我所知),如下所示:
线程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);
可以在输出上产生01
和10
(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
是如何可能的。
声明的下一部分是:
它们只保证原子性和修改顺序的一致性。
这意味着唯一的保证是每个操作都是原子的,也就是说,“中途通过”操作是不可能的。这意味着如果x
是atomic<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的唯一执行是:
这里我用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 ++内存模型一致的执行。