面试题答案
一键面试内存顺序的作用
在Rust的原子操作中,内存顺序定义了原子操作与其他内存操作之间的同步关系。它决定了一个线程中的写操作何时对其他线程可见,以及不同线程中的操作以何种顺序被观察到。这对于编写正确的并发程序至关重要,因为不正确的内存顺序可能导致数据竞争、未定义行为和难以调试的并发错误。
不同内存顺序对并发程序的影响
- SeqCst(顺序一致性)
- 执行顺序:所有线程对原子操作的观察顺序一致,就好像所有的原子操作都在一个全局的顺序中依次执行。这个全局顺序与程序顺序(代码中的顺序)一致,所有线程都遵循这个顺序来观察原子操作。
- 结果影响:提供了最强的同步保证,确保所有线程看到相同的操作顺序,避免了许多并发错误。但它也是最昂贵的内存顺序,因为它需要在多处理器系统中进行大量的同步操作。
- 示例:
use std::sync::atomic::{AtomicUsize, Ordering};
let data = AtomicUsize::new(0);
let flag = AtomicUsize::new(0);
std::thread::spawn(move || {
data.store(42, Ordering::SeqCst);
flag.store(1, Ordering::SeqCst);
});
while flag.load(Ordering::SeqCst) == 0 {
std::thread::yield_now();
}
assert_eq!(data.load(Ordering::SeqCst), 42);
- 在这个例子中,使用
SeqCst
确保了data
的存储操作在flag
的存储操作之前完成,并且另一个线程在看到flag
为1时,一定能看到data
已经被设置为42。
- Relaxed(宽松顺序)
- 执行顺序:对原子操作的顺序没有任何跨线程的同步保证。它只保证原子操作本身的原子性,即不会出现部分写入或读取的情况。不同线程对这些原子操作的观察顺序可能是任意的。
- 结果影响:适合于不需要跨线程同步,但需要原子性的场景,例如简单的计数器。由于没有同步开销,性能较好,但容易引入并发错误,如果使用不当。
- 示例:
use std::sync::atomic::{AtomicUsize, Ordering};
let counter = AtomicUsize::new(0);
std::thread::spawn(move || {
for _ in 0..1000 {
counter.fetch_add(1, Ordering::Relaxed);
}
});
for _ in 0..1000 {
counter.fetch_add(1, Ordering::Relaxed);
}
// 这里不能保证counter的值一定是2000,因为Relaxed顺序没有同步保证
println!("Counter value: {}", counter.load(Ordering::Relaxed));
- Acquire/Release
- 执行顺序:
- Release:当一个线程对一个原子变量执行Release存储操作时,它保证在此之前的所有内存操作(包括普通的读写操作)在该存储操作完成之前都已经完成,并且对其他线程可见。
- Acquire:当一个线程对一个原子变量执行Acquire加载操作时,它保证在此之后的所有内存操作(包括普通的读写操作)在该加载操作完成之后才会开始,并且能看到之前其他线程通过Release存储操作发布的所有内存更新。
- 结果影响:提供了一种比
SeqCst
轻量级的同步方式,适用于许多常见的并发场景,如生产者 - 消费者模型。 - 示例:
- 执行顺序:
use std::sync::atomic::{AtomicUsize, Ordering};
let data = AtomicUsize::new(0);
let flag = AtomicUsize::new(0);
std::thread::spawn(move || {
data.store(42, Ordering::Release);
flag.store(1, Ordering::Release);
});
while flag.load(Ordering::Acquire) == 0 {
std::thread::yield_now();
}
assert_eq!(data.load(Ordering::Acquire), 42);
- 在这个例子中,
Release
存储操作保证了data
的存储在flag
存储之前完成,并且Acquire
加载操作保证了在看到flag
为1时,能看到data
已经被设置为42。
根据具体需求选择合适的内存顺序
- 性能优先,不需要跨线程同步:如果只是需要原子性,例如简单的本地计数器,使用
Relaxed
顺序可以获得最佳性能。 - 生产者 - 消费者模型等常见并发场景:
Acquire/Release
顺序提供了足够的同步保证,同时开销比SeqCst
小,适合这类场景。 - 需要严格的全局顺序一致性:当需要所有线程看到相同的操作顺序,例如实现一个全局的资源分配器时,使用
SeqCst
顺序,但要注意性能开销。