在开发中用到了 AtomicU8,但只会用 Ordering::SeqCst,不知道原理,更不知道和 Ordering::Relaxed 等其他排序方式的区别。
于是,跟着官方文档读了 nomicon,了解了编译重排和硬件重排会让代码执行出现乱序的情况。
读完又读了 <Why Memory Barriers?>,知道硬件重排是指为了不阻塞而设计了 store buffer 和 invalidate queue,如果不加以控制就会出现指令执行和内存读取的乱序情况。
最后,试着把这些联系起来理解,但是没明白。
所以请教大家:怎么联系和理解 Ordering 与内存屏障的关系?能不能举个例子?
Ext Link: https://doc.rust-lang.org/std/sync/atomic/enum.Ordering.html
评论区
写评论内存模型:
内存一致性模型定义了程序执行的结果可能会表现出哪些行为。最符合直觉的是顺序一致性模型(程序的任意执行的结果和在处理器上执行的所有操作按照某种顺序排序执行的结果一样,并且每个处理器上的操作都会按照程序指定的顺序出现的操作序列中)。但是现实是各个平台都是弱有序的,并且强度也不一致,因此作为程序员来说,我们只需要关注编程语言级别定义的内存一致性模型即可,当我们指定原子操作的 Memory Ordering 时,由编译器负责将其映射到不同的平台上。例如 X86 是强有序的,而 Arm 是弱有序的,如果我们指定使用 Release Ordering,在 X86 上可能会编译为一条普通的指令,而在 Arm 上编译器才会为我们插入相应的内存屏障。
同步模式:
通常来说存在两种同步模式:Release/Acquire 和 SeqCst。以 Release/Acquire 为例,假设我们有下面的程序:
假设 Flag 和 X 的初值都等于0,由于CPU/编译器重排序或缓存问题,thread2 执行的断言 assert_eq!(X, 1) 可能会失败,即 assert_eq!(X, 1) 中 X 可能会读到 old value 0。为了避免这个问题,需要使用 Release/Acquire同步 建立 happen-before 关系,即:
这样 Flag.store(1, Release) happens before Flag.load(Acquire),Flag.store之前的所有指令对 Flag.load 之后的所有指令可见,那么断言 assert_eq!(X, 1) 必然成功。
第二种同步模式 SeqCst 表示程序中应该完全排序的点,这时候程序的正确性却决于 SecCst fence 的交错属性。
总的来说,不需要过多地了解底层的各种优化之类的,只需要关注编程语言级别的语义定义。
可以参考这篇文章Memory Model: 从多处理器到高级语言。
我的理解是(不一定正确):
重排序是默认行为,只能保证在单线程下的正确性,不能保证多线程的正确性。
内存屏障指的是一类指令,屏障是指它的效果,即阻止其前后的指令越过自身(重排序)。
Ordering 是对重排序要求的具体描述,用于指定在此处允许和禁止的重排序类型。
要保证多线程程序的正确性,需要编码人员选择恰当的 Ordering,编译器会根据 Ordering 类型插入相应的屏障指令,抑制重排序行为。
不同硬件默认使用不同等级的重排序,并提供相应的屏障指令来抑制这种行为;重排序等级越高,对内存序的保证越弱,可以进行更激进的优化。
使用较强的 Ordering 有利于保证程序正确性,但是在内存序更弱的硬件上会导致性能损失;使用较弱的 Ordering 有利于性能,但更容易出错,而且在内存序更强的硬件上没有效果(硬件默认提供了更强的保证)。
由于编译器重排序的存在,高级语言内存模型几乎总是比硬件内存模型更弱,相应的也有编译器屏障的存在,用于抑制编译器重排序,所有内存屏障都隐式包含了相应的编译器屏障。
https://rustcc.cn/article?id=3259fdf2-9caa-4bf6-a835-6d58efe2f9ee
前些天做过实验,复现了Relaxed参数确实有可能出现不符合预期的结果。
可以看看 Rust Atomics and Locks,专门有一章是讲 memory ordering 的