[英]Reordering and memory_order_relaxed
Cppreference给出以下有关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
的修改顺序出现在A之前,而B是以x
的修改顺序出现在C之前。 现在我的问题是:如果不能在线程1中对A和B进行重新排序,并且类似地在线程2中对C和D进行重新排序(因为它们中的每一个都在其线程内先后排序 ), 那么点1和点2是否不矛盾 ? 换句话说, 在没有重新排序的情况下 (似乎要指出第1点),如何甚至可视化下面第2点中的情况?
T1 ...... T2
.............. D(y)
A(y)的
B(x)的
.............. C(x)
因为在这种情况下C将不被测序-之前线程2内d,如点1级的要求。
无需重新排序(如第1点所示)
点1并不意味着“不重新排序”。 这意味着在执行线程中对事件进行排序。 编译器将在B之前向A发出CPU指令,而在D之前向C发出CPU指令(尽管即使被as-if规则颠覆),但CPU也没有义务按照该顺序执行它们,即缓存/写入缓冲区/无效队列没有义务按该顺序传播,并且内存也没有统一的义务。
(尽管个别架构可以提供这些保证)
根据这篇文章的STR类推: C ++ 11引入了标准化的内存模型。 这是什么意思? 它将如何影响C ++编程? ,我创建了可视化效果图(据我了解),如下所示:
线程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原子操作不会发生这样的输出)。
您对文本的解释是错误的。 让我们分解一下:
标记为
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
和values
属性是同步的。
仅查看C ++内存模型(不讨论编译器或硬件重新排序),导致r1 = r2 = 42的唯一执行是:
在这里,我将r1替换为a,将r2替换为b。 像往常一样,sb代表先于顺序,并且仅是线程间排序(指令在源代码中出现的顺序)。 rf是“ Read-From”边缘,表示一端的“读取/加载”读取另一端写入/存储的值。
对于循环来说,包含sb和rf边缘的循环(以绿色突出显示)对于结果是必不可少的:y被写在一个线程中,在另一个线程中被读取到a中,然后从那里被写入x,在前者中被读取。再次线程到b(在写入y之前先排序)。
为什么不能像这样构造图有两个原因:因果关系和因为RF读取了隐藏的副作用。 在这种情况下,后者是不可能的,因为我们只向每个变量写入一次,因此很明显一个写入不能被另一个写入隐藏(覆盖)。
为了回答因果关系问题,我们遵循以下规则:当循环涉及单个内存位置且sb边的方向在循环中的每个位置都在同一方向(rf边的方向)时,不允许循环(不可能)在这种情况下不相关); 或者,该循环涉及多个变量,所有边沿(sb和rf)都在同一方向上,并且至少一个变量在不释放/获取的不同线程之间具有一个或多个rf边沿。
在这种情况下,存在循环,涉及两个变量(x的一个rf边和y的一个rf边),所有边都在同一方向上,但是两个变量具有松弛/松弛的rf边(即x和y)。 因此,没有因果关系违规,并且此执行与C ++内存模型一致。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.