Rust标准库中的原子类型
https://doc.rust-lang.org/std/sync/atomic/index.html
原子类型
原子类型提供了线程之间的原始共享内存通信,也是其他并发类型的构件。
这个模块定义了一些精选的原始类型的原子版本,包括AtomicBool、AtomicIsize、AtomicUsize、AtomicI8、AtomicU16等。Atomic类型呈现的操作,正确使用时,可以在线程之间同步更新。
每个方法都取 Ordering
,它代表该操作的内存屏障强度。这些顺序与C++20的原子顺序相同。更多信息,请参见nomicon。
原子变量在线程之间共享是安全的(它们实现了Sync),但它们本身并没有提供共享的机制,遵循Rust的线程模型。共享原子变量最常见的方式是将其放入Arc(原子引用计数的共享指针)。
原子类型可以存储在静态变量中,使用像 AtomicBool:::new 这样的常量初始化器进行初始化。原子静态常量经常被用于延迟的全局初始化。
可移植性
这个模块中的所有原子类型都保证是无锁的,只要可用。这意味着它们不会在内部使用全局mutex。原子类型和操作不保证是无等待的。这意味着像 fetch_or 这样的操作可以通过比较和交换循环(compare-and-swap)来实现。
原子操作可以在指令层用较大尺寸的原子类型来实现。例如有些平台使用4字节的原子指令来实现AtomicI8。注意,这种模拟应该不会对代码的正确性产生影响,只是需要注意的地方。
这个模块中的原子类型可能并不是所有平台都能使用。但是,这里的原子类型都是广泛可用的,一般来说,可以依赖现有的原子类型。一些值得注意的例外是。
-
具有32位指针的PowerPC和MIPS平台没有AtomicU64或AtomicI64类型。
-
非Linux的ARM平台,如armv5te,根本就没有AtomicU64或AtomicI64类型。
-
采用thumbv6m的ARM目标完全没有原子操作。
需要注意的是,未来可能会加入同样不支持某些原子操作的平台。最大限度的可移植代码要注意使用哪些原子类型。一般来说,AtomicUsize和AtomicIsize是最可移植的,但即便如此,也不是到处都有。作为参考,std库需要指针大小的原子类型,虽然core不需要。
目前你需要使用#[[cfg(target_arch)]]主要是为了有条件地在代码中编译原子体。还有一个不稳定的#[cfg(target_has_atomic)]也是不稳定的,将来可能会稳定下来。
例子
一个简单的自旋锁:
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::thread;
fn main() {
let spinlock = Arc::new(AtomicUsize::new(1));
let spinlock_clone = spinlock.clone();
let thread = thread::spawn(move|| {
spinlock_clone.store(0, Ordering::SeqCst);
});
// Wait for the other thread to release the lock
while spinlock.load(Ordering::SeqCst) != 0 {}
if let Err(panic) = thread.join() {
println!("Thread had an error: {:?}", panic);
}
}
保持全局的活跃线程数:
use std::sync::atomic::{AtomicUsize, Ordering};
static GLOBAL_THREAD_COUNT: AtomicUsize = AtomicUsize::new(0);
let old_thread_count = GLOBAL_THREAD_COUNT.fetch_add(1, Ordering::SeqCst);
println!("live threads: {}", old_thread_count + 1);
Ordering枚举
https://doc.rust-lang.org/std/sync/atomic/enum.Ordering.html
pub enum Ordering {
Relaxed,
Release,
Acquire,
AcqRel,
SeqCst,
}
内存顺序指定了原子操作同步内存的方式。在其最弱的Relaxed中,只有操作直接接触到的内存才会被同步。另一方面,SeqCst操作的存储-加载对在同步其他内存的同时,还额外保留了所有线程中此类操作的总顺序。
Rust的内存顺序与C++20的内存顺序相同。
更多的信息请参见nomicon。
-
Relaxed
没有排序约束,只有原子操作。
对应于C++20中的 memory_order_relaxed。
-
Release
当与存储结合在一起时,所有之前的操作都会在加载该值的任何 Acquire(或更强的)排序之前变得有序。特别是,所有之前的所有写操作都会对执行该值的Acquire(或更强)加载的线程可见。
请注意,对结合了加载和存储的操作使用此命令会导致 Relaxed 加载操作!
这个命令只适用于可以执行存储的操作。
对应于C++20中的 memory_order_release。
-
Acquire
当与加载结合在一起时,如果加载的值是由具有Release(或更强的)排序的存储操作写入的,那么后续的所有操作都会在该存储之后成为排序。特别是,所有后续的加载操作都会看到在存储操作之前写入的数据。
请注意,对一个结合了加载和存储的操作使用这种排序会导致一个Relaxed存储操作!
这个命令只适用于可以执行加载的操作。
对应于C++20中的memory_order_acquire。
-
AcqRel
同时具有Acquire和Release的效果。对于负载,它使用的是Acquire命令。对于存储,它使用的是Release命令。
注意,在compare_and_swap的情况下,有可能操作最终没有执行任何存储,因此它只执行Acquire命令。然而,AcqRel永远不会执行Relaxed访问。
这种命令只适用于同时执行加载和存储的操作。
对应于C++20中的memory_order_acq_rel。
-
SeqCst
类似于Acquire/Release/AcqRel(分别用于加载、存储和加载与存储操作),额外保证了所有线程以相同的顺序查看所有顺序一致的操作。
对应于C++20中的memory_order_seq_cst。
compiler_fence函数
pub fn compiler_fence(order: Ordering)
编译器的内存围栏。
compiler_fence不发出任何机器代码,但限制了编译器允许做的内存重排序的种类。具体来说,根据给定的 Ordering 语义,编译器可能会被禁止从调用 compiler_fence 之前或之后的读或写移动到调用 compiler_fence 的另一端。注意,这并不妨碍硬件进行这种重新排序。这在单线程、执行上下文中不是问题,但当其他线程可能同时修改内存时,需要更强的同步基元(如fence)。
不同的排序语义所阻止的重排序是。
- 用SeqCst,不允许跨这个点的读和写的重新排序。
- 使用Release,前面的读和写不能越过后续的写。
- 使用Acquire,后续的读和写不能在前面的读之前移动。
- 使用AcqRel,以上两条规则都会被执行。
compiler_fence通常只对防止线程与自己赛跑有用。也就是说,如果一个给定的线程正在执行一段代码,然后被中断,并开始执行其他地方的代码(同时仍然在同一个线程中,并且在概念上仍然在同一个内核上)。在传统程序中,这种情况只有在注册了信号处理程序后才会出现。在更多的底层代码中,在处理中断时、实现绿色线程预置时等也会出现这种情况。鼓励好奇的读者阅读Linux内核中关于内存障碍的讨论。
例子:
如果没有 compiler_fence,下面的代码中的assert_eq!中的assert_eq!不能保证成功,尽管一切都发生在一个线程中。要了解原因,请记住,编译器可以自由地将存储空间调换成import_variable和is_read,因为它们都是Ording:::Relaxed。如果它这样做了,并且信号处理程序在IS_READY被更新后立即被调用,那么信号处理程序将看到IS_READY=1,但IMPORTANT_VARIABLE=0。 使用compiler_fence可以纠正这种情况。
use std::sync::atomic::{AtomicBool, AtomicUsize};
use std::sync::atomic::Ordering;
use std::sync::atomic::compiler_fence;
static IMPORTANT_VARIABLE: AtomicUsize = AtomicUsize::new(0);
static IS_READY: AtomicBool = AtomicBool::new(false);
fn main() {
IMPORTANT_VARIABLE.store(42, Ordering::Relaxed);
// prevent earlier writes from being moved beyond this point
compiler_fence(Ordering::Release);
IS_READY.store(true, Ordering::Relaxed);
}
fn signal_handler() {
if IS_READY.load(Ordering::Relaxed) {
assert_eq!(IMPORTANT_VARIABLE.load(Ordering::Relaxed), 42);
}
}
fence函数
https://doc.rust-lang.org/std/sync/atomic/fn.fence.html
pub fn fence(order: Ordering)
一个原子栅栏。
根据指定的顺序,栅栏可以防止编译器和CPU围绕着它重新排序某些类型的内存操作。这就在它和其他线程中的原子操作或栅栏之间建立了同步关系。
一个具有(至少)释放排序语义的栅栏’A’与具有(至少)获得语义的栅栏’B’同步,如果且仅当存在操作X和Y,都在某些原子对象’M’上操作,那么A在X之前被排序,Y在B之前被同步,而Y观察到M的变化。
Thread 1 Thread 2
fence(Release); A --------------
x.store(3, Relaxed); X --------- |
| |
| |
-------------> Y if x.load(Relaxed) == 3 {
|-------> B fence(Acquire);
...
}
具有Release或Acquire语义的原子操作也可以与栅栏同步。
一个具有SeqCst排序的栅栏,除了具有Acquire和Release语义外,还可以参与其他SeqCst操作和/或栅栏的全局程序排序。
接受Acquire、Release、AcqRel和SeqCst命令。
例子:
use std::sync::atomic::AtomicBool;
use std::sync::atomic::fence;
use std::sync::atomic::Ordering;
// 基于自旋锁的互斥基元。
pub struct Mutex {
flag: AtomicBool,
}
impl Mutex {
pub fn new() -> Mutex {
Mutex {
flag: AtomicBool::new(false),
}
}
pub fn lock(&self) {
while !self.flag.compare_and_swap(false, true, Ordering::Relaxed) {}
// This fence synchronizes-with store in `unlock`.
fence(Ordering::Acquire);
}
pub fn unlock(&self) {
self.flag.store(false, Ordering::Release);
}
}
spin_loop_hint函数
https://doc.rust-lang.org/std/sync/atomic/fn.spin_loop_hint.html
pub fn spin_loop_hint()
向处理器发出信号,表示它处于忙等待的自旋循环(“spin lock”)中。
接收到自旋循环信号后,处理器可以通过节省功耗或切换超线程等方式优化自己的行为。
这个函数不同于 std::thread::yield_now,后者直接向系统的调度器输出,而spin_loop_hint不与操作系统交互。
spin_loop_hint的一个常见用例是在同步基元中实现CAS循环中的约束优化旋转。为了避免优先级反转等问题,强烈建议在有限的迭代次数后终止自旋循环,并进行适当的阻塞syscall。
注意:在不支持接收自旋循环提示的平台上,这个函数根本不做任何事情。