存储是对于两者都是释放操作,加载则是对于两者都是获取操作。我知道memory_order_seq_cst
旨在为所有操作强制实施额外的全序,但我无法构建一个示例,在其中用memory_order_acq_rel
替换所有memory_order_seq_cst
而不是这种情况。
我错过了什么,还是只是文档效应的差异,即如果不打算使用更轻松的模型并使用memory_order_acq_rel
来约束轻松的模型,则应该使用memory_order_seq_cst
?
存储是对于两者都是释放操作,加载则是对于两者都是获取操作。我知道memory_order_seq_cst
旨在为所有操作强制实施额外的全序,但我无法构建一个示例,在其中用memory_order_acq_rel
替换所有memory_order_seq_cst
而不是这种情况。
我错过了什么,还是只是文档效应的差异,即如果不打算使用更轻松的模型并使用memory_order_acq_rel
来约束轻松的模型,则应该使用memory_order_seq_cst
?
http://en.cppreference.com/w/cpp/atomic/memory_order有一个很好的例子在底部, 仅适用于memory_order_seq_cst
。基本上,memory_order_acq_rel
提供相对于原子变量的读写顺序,而memory_order_seq_cst
提供全局的读写顺序。也就是说,顺序一致的操作在所有线程中以相同的顺序可见。
这个例子可以归结为:
bool x= false;
bool y= false;
int z= 0;
a() { x= true; }
b() { y= true; }
c() { while (!x); if (y) z++; }
d() { while (!y); if (x) z++; }
// kick off a, b, c, d, join all threads
assert(z!=0);
z
上的操作由两个原子变量保护,而不是一个,因此您不能使用获取-释放语义来强制执行始终递增z
。
seq_cst
存储需要刷新存储缓冲区,因此该线程的后续读取将延迟到存储全局可见之后。(至少在下一个seq_cst
加载之前,不一定在非原子或非SC原子加载或存储之前。)
有关AArch64对此的硬件支持,请参见下文;大多数其他ISA只需等待存储缓冲区在SC存储和RMW之后排空。一种C++到汇编的替代映射是在每个seq_cst加载之前排空存储缓冲区,而不是在存储之后,但我们更需要廉价的加载而不是廉价的存储。(多个核心可以并行加载同一对象;存储必然导致争用以获得缓存行的独占所有权。)
acquire
或release
不必刷新存储缓冲区。普通的x86加载和存储基本上具有acq和rel语义,在x86汇编中是免费的。(每个核心对一致性缓存的访问按程序顺序进行,除了具有存储转发的存储缓冲区。)
但是x86原子RMW操作总是被“提升”为seq_cst
,因为x86汇编的lock
前缀是一个完全的内存屏障。其他ISA可以在汇编中执行松散或acq_rel
的RMW操作,存储方面可以对其他对象的后续存储或加载进行有限的重新排序。(但不能以使RMW看起来非原子的方式进行重新排序:为了排序,原子读取-修改-写是一个操作还是两个操作?)
mov
+ mfence
与普通的mov
的区别。在实践中,xchg
是大多数x86 CPU上执行seq_cst存储更高效的方式,但GCC确实使用mov
+mfence
)
machine_clears.memory_ordering
性能计数器:Why flush the pipeline for Memory Order Violation caused by other logical processors?) - Peter Cordes尝试仅使用acquire/release语义构建Dekkers或Peterson算法。
这不会起作用,因为Acquire/Release语义不能提供[StoreLoad]屏障。
对于Dekkers算法:
flag[self]=1 <-- STORE
while(true){
if(flag[other]==0) { <--- LOAD
break;
}
flag[self]=0;
while(turn==other);
flag[self]=1
}
没有 [StoreLoad] 栅栏,存储操作可能会跳过加载操作导致算法失败。同时,两个线程可能会同时看到另一个锁是空闲的,设置自己的锁并继续执行。这样,你就会有两个线程处于临界区。
x.store(true, std::memory_order_release); // thread0
y.store(true, std::memory_order_release); // thread1
if (y.load(std::memory_order_acquire)) { ++z; } // thread2, load y first
while (!x.load(std::memory_order_acquire)); // and then, load x
if (x.load(std::memory_order_acquire)) { ++z; } // thread3, load x first
while (!y.load(std::memory_order_acquire)); // and then, load y
因此,如果在thread0和thread1之前执行了thread2和thread3,那么x和y都保持为false,因此++z永远不会被触及,z保持为0并且assert失败。
然而,如果使用memory_order_seq_cst,则建立所有标记为这样的原子操作的单个总修改顺序。 因此,在thread2中,x.load然后y.load; 在thread3中,y.load然后x.load是确定的事情。
c()
可以感知在a()
中x=true;
发生在b()
中y=true;
之前,同时,由于缺乏“全局排序”,d()
也可以感知y=true;
发生在x=true;
之前。特别地,c()
可以感知x==true
和y==false
同时成立,而d()
可以感知y==true
和x==false
同时成立。因此,z
可能既不会被c()
增加,也不会被d()
增加。使用seq_cst时,如果c()
感知到x=true;
在y=true;
之前发生,那么d()
也会这样感知。 - nodakaiint z=0
,而不是bool z=0
。 - nodakaistd::memory_order_acq_rel
的原子读写修改操作。在原子读改写操作中,加载和存储是绑定在一起的,因为它们是一个原子操作。我不确定像.fetch_add
或.compare_exchange_weak
这样的操作什么时候(如果有时候)acq_rel
会与seq_cst
不同。 - Peter Cordes