问题 C ++内存模型中的哪些确切规则会阻止在获取操作之前重新排序?
我对以下代码中的操作顺序有疑问:
std::atomic<int> x;
std::atomic<int> y;
int r1;
int r2;
void thread1() {
y.exchange(1, std::memory_order_acq_rel);
r1 = x.load(std::memory_order_relaxed);
}
void thread2() {
x.exchange(1, std::memory_order_acq_rel);
r2 = y.load(std::memory_order_relaxed);
}
给出描述 std::memory_order_acquire
在cppreference页面上(https://en.cppreference.com/w/cpp/atomic/memory_order那个
具有此内存顺序的加载操作会对受影响的内存位置执行获取操作:在此加载之前,不能对当前线程中的读取或写入进行重新排序。
很明显,永远不会有结果 r1 == 0 && r2 == 0
跑完之后 thread1
和 thread2
同时。
但是,我在C ++标准中找不到任何措辞(现在查看C ++ 14草案),这保证了两个宽松的加载不能与获取 - 释放交换重新排序。我错过了什么?
编辑:正如评论中所建议的那样,实际上可以使r1和r2都等于零。我已经更新了程序以使用load-acquire,如下所示:
std::atomic<int> x;
std::atomic<int> y;
int r1;
int r2;
void thread1() {
y.exchange(1, std::memory_order_acq_rel);
r1 = x.load(std::memory_order_acquire);
}
void thread2() {
x.exchange(1, std::memory_order_acq_rel);
r2 = y.load(std::memory_order_acquire);
}
现在可以得到两者兼而有之 r1
和 r2
并发执行后等于0 thread1
和 thread2
?如果没有,哪些C ++规则阻止了这个?
6990
2017-10-02 10:27
起源
答案:
该标准没有根据如何使用特定排序参数围绕原子操作排序操作来定义C ++内存模型。
相反,对于获取/发布排序模型,它定义了正式关系,例如“synchronize-with”和“before-before”,用于指定线程之间数据的同步方式。
N4762,§29.4.2 - [atomics.order]
对原子对象M执行释放操作的原子操作A与对M执行获取操作的原子操作B同步
并以A为首的释放序列中的任何副作用取其值。
在§6.8.2.1-9中,该标准还规定,如果存储A与负载B同步,则在A线程之前排序的任何内容“发生在B之后的任何顺序之前”。
在第二个示例(第一个甚至更弱)中没有建立“synchronize-with”(因此发生线程间发生)关系,因为缺少运行时关系(检查来自负载的返回值)。
但即使你确实检查了返回值,它也没有用 exchange
操作实际上并不“释放”任何东西(即在这些操作之前没有对存储器操作进行排序)。
Neiter做原子加载操作'获取'任何东西,因为在加载后没有操作排序。
因此,根据标准,两个示例(包括0 0)中的负载的四种可能结果中的每一种都是有效的。
事实上,标准给出的保证并不强于 memory_order_relaxed
在所有操作上。
如果要在代码中排除0 0结果,则必须使用所有4个操作 std::memory_order_seq_cst
。这保证了相关操作的单一总订单。
5
2017-10-03 06:02
在原始版本中,可以看到 r1 == 0 && r2 == 0
因为没有要求商店在读取之前传播给其他线程。这不是一个 重新排序 任何线程的操作,但例如读取过时的缓存。
Thread 1's cache | Thread 2's cache
x == 0; | x == 0;
y == 0; | y == 0;
y.exchange(1, std::memory_order_acq_rel); // Thread 1
x.exchange(1, std::memory_order_acq_rel); // Thread 2
线程2忽略线程1上的版本,反之亦然。在抽象机器中,与值不一致 x
和 y
在线程上
Thread 1's cache | Thread 2's cache
x == 0; // stale | x == 1;
y == 1; | y == 0; // stale
r1 = x.load(std::memory_order_relaxed); // Thread 1
r2 = y.load(std::memory_order_relaxed); // Thread 2
你需要 更多 通过获取/释放对获得“违反因果关系”的线程,正常的排序规则,与“成为可见的副作用”规则相结合,强制至少一个 load
要看 1
。
不失一般性,让我们假设线程1首先执行。
Thread 1's cache | Thread 2's cache
x == 0; | x == 0;
y == 0; | y == 0;
y.exchange(1, std::memory_order_acq_rel); // Thread 1
Thread 1's cache | Thread 2's cache
x == 0; | x == 0;
y == 1; | y == 1; // sync
线程1上的发布与线程2上的获取形成一对,抽象机器描述了一致性 y
在两个线程上
r1 = x.load(std::memory_order_relaxed); // Thread 1
x.exchange(1, std::memory_order_acq_rel); // Thread 2
r2 = y.load(std::memory_order_relaxed); // Thread 2
2
2017-10-02 15:05
在 发布 - 获取订购 为了在2个线程之间创建同步点,我们需要一些原子对象 M
这将是 相同 在这两个行动中
原子操作 A
在...上执行释放操作
原子对象 M
与原子操作同步 B
执行获取操作 M
从任何人那里获取其价值
释放顺序中的副作用 A
。
或者更详细地说:
如果一个原子商店在线程中 A
被标记 memory_order_release
和线程中的原子加载 B
从相同的变量标记
memory_order_acquire
,所有内存写入(非原子和放松
原子)发生在原子商店之前的观点
线程 A
,成为线程中可见的副作用 B
。那
是,一旦原子加载完成,线程 B
保证
看到一切线程 A
写信给记忆。
仅在释放的线程之间建立同步
并获得 相同 原子变量。
N = u | if (M.load(acquire) == v) :[B]
[A]: M.store(v, release) | assert(N == u)
这里有同步点 M
store-release和load-acquire(从store-release获取值!)。作为结果存储 N = u
在线程中 A
(在商店发布之前 M
)可见 B
(N == u
)在加载后获得相同的 M
如果举个例子:
atomic<int> x, y;
int r1, r2;
void thread_A() {
y.exchange(1, memory_order_acq_rel);
r1 = x.load(memory_order_acquire);
}
void thread_B() {
x.exchange(1, memory_order_acq_rel);
r2 = y.load(memory_order_acquire);
}
我们可以为常见的原子对象选择什么 M
?说 x
? x.load(memory_order_acquire);
将与同步点 x.exchange(1, memory_order_acq_rel)
( memory_order_acq_rel
包括 memory_order_release
(更强)和 exchange
包括 store
)如果 x.load
从中加载值 x.exchange
和main将是同步加载 后 通过商店获得(在获得任何存在之后的代码中) 之前 释放(但在代码之前没有交换之前)。
正确的解决方案(几乎完全找到 题 )可以是下一个:
atomic<int> x, y;
int r1, r2;
void thread_A()
{
x.exchange(1, memory_order_acq_rel); // [Ax]
r1 = y.exchange(1, memory_order_acq_rel); // [Ay]
}
void thread_B()
{
y.exchange(1, memory_order_acq_rel); // [By]
r2 = x.exchange(1, memory_order_acq_rel); // [Bx]
}
假使,假设 r1 == 0
。
对任何特定原子变量的所有修改都在总计中发生
特定于这一个原子变量的顺序。
我们有2个修改 y
: [Ay]
和 [By]
。因为 r1 == 0
这意味着 [Ay]
发生在之前 [By]
总修改顺序 y
。由此 - [By]
读取存储的值 [Ay]
。所以我们接下来:
A
写信给 x
- [Ax]
A
做商店发布 [Ay]
至 y
在这之后 ( acq_rel 包括 发布,
交换 包括 商店)
B
从中获取负载 y
([By]
存储的值 [Ay]
- 一旦原子载荷获得(上
y
)完成,线程 B
是
保证看到一切线程 A
之前写过记忆
商店发布(上 y
)。所以它认为副作用 [Ax]
- 和 r2 == 1
另一种可能的解决方案 atomic_thread_fence
atomic<int> x, y;
int r1, r2;
void thread_A()
{
x.store(1, memory_order_relaxed); // [A1]
atomic_thread_fence(memory_order_acq_rel); // [A2]
r1 = y.exchange(1, memory_order_relaxed); // [A3]
}
void thread_B()
{
y.store(1, memory_order_relaxed); // [B1]
atomic_thread_fence(memory_order_acq_rel); // [B2]
r2 = x.exchange(1, memory_order_relaxed); // [B3]
}
再次因为原子变量的所有修改 y
按总顺序发生。 [A3]
将在之前 [B1]
或反之亦然。
如果 [B1]
之前 [A3]
- [A3]
读取存储的值 [B1]
=> r1 == 1
。
如果 [A3]
之前 [B1]
- [B1]
是由...存储的读取值 [A3]
从 围栏栅栏同步:
释放栅栏 [A2]
在线程中 A
与获取围栏同步 [B2]
在线程中 B
,如果:
在这种情况下,所有商店([A1]
)之前是排序的 [A2]
在线程中 A
将在所有负载之前发生([B3]
)来自相同的位置(x
)线程制作 B
后 [B2]
所以 [A1]
(商店1到x)将在之前并且具有可见效果 [B3]
(加载表单x并将结果保存到 r2
)。所以将被加载 1
从 x
和 r2==1
[A1]: x = 1 | if (y.load(relaxed) == 1) :[B1]
[A2]: ### release ### | ### acquire ### :[B2]
[A3]: y.store(1, relaxed) | assert(x == 1) :[B3]
2
2017-10-03 13:00
你已经有了语言律师部分的答案。但我想回答一个相关的问题,即如何理解为什么在可能的CPU架构中使用asm是可能的 LL / SC用于RMW原子。
C ++ 11禁止这种重新排序是没有意义的:在这种情况下需要存储加载障碍,其中一些CPU架构可以避免一个。
考虑到他们将C ++ 11内存命令映射到asm指令的方式,它实际上可能适用于PowerPC上的真实编译器。
在PowerPC64上,具有acq_rel交换和获取加载的函数(使用指针args而不是静态变量)编译如下 gcc6.3 -O3 -mregnames
。这是来自C11版本,因为我想查看MIPS和SPARC的clang输出,而Godbolt的clang设置适用于C11 <atomic.h>
但C ++ 11失败了 <atomic>
当你使用 -target sparc64
。
(来源+ asm 在Godbolt上用于MIPS32R6,SPARC64,ARM 32和PowerPC64。)
foo:
lwsync # with seq_cst exchange this is full sync, not just lwsync
# gone if we use exchage with mo_acquire or relaxed
# so this barrier is providing release-store ordering
li %r9,1
.L2:
lwarx %r10,0,%r4 # load-linked from 0(%r4)
stwcx. %r9,0,%r4 # store-conditional 0(%r4)
bne %cr0,.L2 # retry if SC failed
isync # missing if we use exchange(1, mo_release) or relaxed
ld %r3,0(%r3) # 64-bit load double-word of *a
cmpw %cr7,%r3,%r3
bne- %cr7,$+4 # skip over the isync if something about the load? PowerPC is weird
isync # make the *a load a load-acquire
blr
isync
不是商店装载障碍;它只需要在本地完成上述指令(从核心的无序部分退出)。它不会等待刷新存储缓冲区,因此其他线程可以看到早期的存储。
因此SC(stwcx.
存储交换的一部分可以位于存储缓冲区中并变为全局可见 后跟随它的纯粹的获取负载。 事实上,另一个Q&A已经问过这个,答案是我们认为这种重新排序是可能的。 `isync`是否会阻止CPU PowerPC上的Store-Load重新排序?
如果纯负荷是 seq_cst
,PowerPC64 gcc放一个 sync
之前 ld
。制作 exchange
seq_cst
不 不 防止重新排序。请记住,C ++ 11仅保证SC操作的单个总订单,因此交换和加载都需要为C ++ 11的SC来保证它。
所以PowerPC有一些从C ++ 11到asm for atomics的不寻常的映射。大多数系统在商店上放置较重的障碍,允许seq-cst负载更便宜或仅在一侧具有屏障。我不确定这是否是PowerPC着名的弱内存排序所必需的,或者是否可能有其他选择。
https://www.cl.cam.ac.uk/~pes20/cpp/cpp0xmappings.html 展示了各种架构的一些可能的实现。它提到了ARM的多种替代方案。
在AArch64上,我们得到了原始C ++版本的thread1:
thread1():
adrp x0, .LANCHOR0
mov w1, 1
add x0, x0, :lo12:.LANCHOR0
.L2:
ldaxr w2, [x0] @ load-linked with acquire semantics
stlxr w3, w1, [x0] @ store-conditional with sc-release semantics
cbnz w3, .L2 @ retry until exchange succeeds
add x1, x0, 8 @ the compiler noticed the variables were next to each other
ldar w1, [x1] @ load-acquire
str w1, [x0, 12] @ r1 = load result
ret
重新排序不可能在那里发生,因为AArch64发布商店是 顺序 - 发布,不是简单发布。这意味着他们无法对以后的加载重新排序。
但是在一个假想的机器上,或者说有一个简单释放的LL / SC原子,很容易看出acq_rel不会阻止以后加载到不同的缓存行,在LL之后但在交换的SC之前变为全局可见。
如果 exchange
使用x86上的单个事务实现,因此加载和存储在内存操作的全局顺序中是相邻的,那么当然不能在以后的操作中重新排序 acq_rel
交换,它基本上相当于 seq_cst
。
但LL / SC并不一定是真正的原子事务来赋予RMW原子性 为那个位置。
事实上,单一的asm swap
指令可以放宽或acq_rel语义。 SPARC64需要 membar
关于它的说明 swap
指令,所以不像x86的 xchg
它本身不是seq-cst。 (SPARC具有非常好的/人类可读的指令助记符,特别是与PowerPC相比。基本上任何东西都比PowerPC更具可读性。)
因此,C ++ 11要求它做到这一点是没有意义的:它会损害CPU上的实现,否则不需要存储加载障碍。
2
2017-10-03 21:35