P.MTH.LKF.02 使用无锁编程时,需要合理选择内存顺序
【描述】
Rust 原子类型使用 C++20
的内存顺序模型 来指定原子操作的内存同步方式,但也不是完全采用此模型。
目前 Rust 引入五种内存顺序:Relaxed / Release / Acquire / AcqRel / SeqCst
。
在无锁编程中,指定正确的内存顺序是很重要很复杂的一件事,这里有一些建议:
- 如果对程序中的原子类型同步方式的判断没有太多信息,建议使用
SeqCst
,它表示顺序一致性,会强制所有线程都同意程序指令以单一全局线性的方式来执行。这样可以保证安全性,但性能有一定损失。 - 如果对无锁实现中线程间发生的数据竞争带来的后果不是特别关心,则可以放心使用
Relaxed
,因为它性能最好。 - 当多个线程之间操作内存中同一个位置有因果关系时,适合使用
Acquire / Release / AcqRel
来配对。比如,线程 A 写 (Release
) 内存中的一个位置,然后线程 B 随后读 (Acquire
) 内存中一个相同的位置,就会产生一个因果关系,所以为了保证 A 的每次写入都能在 B 读取之前被观察到。如果 A 和 B 访问不同内存位置,则没有因果关系。
【正例】
自旋锁中 Acquire / Release
搭配使用的简易示例:
use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use std::thread; fn main() { let lock = Arc::new(AtomicBool::new(false)); // 这个原子类型的值用来表示 "是否获取到锁" // ... distribute lock to threads somehow ... // 线程 A 尝试通过设置为 ture 来获取锁 // // Acquire 内存顺序: // 当与 load 结合使用时,若 load 的值是由具有 Release (或更强) 排序的 store 操作写入的,则所有后续操作 // 都将在该 store 之后进行排序。特别是,所有后续load都将看到在 store 之前写入的数据。 while lock.compare_and_swap(false, true, Ordering::Acquire) { } // 在循环外,意味着已经拿到了锁! // ...访问/操作数据... // 线程A完成了数据操作,释放锁。 // 此处用 Release 内存顺序可以确保线程B在获取锁时能看到线程A释放了锁(对内存写入 false) // // Release 内存顺序: // 当与 store 结合使用时,所有先前的操作都会在使用 Acquire(或更强)排序的任何 load 此值之前排序。 // 特别是,所有先前的写入对执行 Acquire 此值(或更强)load 的线程都可见。 lock.store(false, Ordering::Release); }