面试题答案
一键面试原子操作与内存顺序的关系
原子操作确保对共享变量的读写操作是不可分割的,不会被其他线程干扰。而内存顺序则规定了原子操作与其他内存访问操作之间的执行顺序和可见性规则。不同的内存顺序会影响原子操作在多线程环境下的行为和性能。
不同内存顺序对原子操作行为的影响举例
Ordering::SeqCst
(顺序一致性)- 这是最严格的内存顺序。使用
SeqCst
时,所有线程对原子变量的操作会形成一个全局的顺序,就好像所有线程都按照这个顺序依次执行原子操作一样。 - 示例代码:
- 这是最严格的内存顺序。使用
use std::sync::atomic::{AtomicU32, Ordering};
fn main() {
let shared_value = AtomicU32::new(0);
std::thread::scope(|s| {
s.spawn(|| {
shared_value.fetch_add(1, Ordering::SeqCst);
});
s.spawn(|| {
let value = shared_value.load(Ordering::SeqCst);
assert!(value >= 1);
});
});
}
- 在这个例子中,由于使用了
SeqCst
,第二个线程读取shared_value
时,一定能看到第一个线程写入的值(或者更大的值,如果有其他线程也进行了写入),因为所有线程对shared_value
的操作在全局有一个顺序。
Ordering::Relaxed
(宽松顺序)Relaxed
内存顺序只保证原子操作本身的原子性,不保证任何内存顺序。也就是说,其他线程对这个原子变量的读写操作可能会以任意顺序与当前线程的原子操作交错执行。- 示例代码:
use std::sync::atomic::{AtomicU32, Ordering};
fn main() {
let shared_value = AtomicU32::new(0);
std::thread::scope(|s| {
s.spawn(|| {
shared_value.fetch_add(1, Ordering::Relaxed);
});
s.spawn(|| {
let value = shared_value.load(Ordering::Relaxed);
// 这里有可能读取到0,因为Relaxed不保证顺序
assert!(value >= 0);
});
});
}
- 在这个例子中,第二个线程读取
shared_value
时,有可能读到0,因为Relaxed
内存顺序不保证第二个线程一定能看到第一个线程写入的值。第一个线程的写入操作和第二个线程的读取操作可以以任意顺序交错执行。
Ordering::Release
和Ordering::Acquire
Release
用于写入操作,表示在该原子操作之前的所有内存访问(读或写),对其他线程在获取该值后是可见的。Acquire
用于读取操作,表示在该原子操作之后的所有内存访问(读或写),不会被重排到该原子操作之前。- 示例代码:
use std::sync::{Arc, Mutex};
use std::sync::atomic::{AtomicU32, Ordering};
fn main() {
let shared_value = Arc::new(AtomicU32::new(0));
let other_data = Arc::new(Mutex::new(42));
let shared_value_clone = shared_value.clone();
let other_data_clone = other_data.clone();
std::thread::scope(|s| {
s.spawn(|| {
let mut data = other_data_clone.lock().unwrap();
*data = 100;
shared_value_clone.store(1, Ordering::Release);
});
s.spawn(|| {
while shared_value.load(Ordering::Acquire) == 0 {
std::thread::yield_now();
}
let data = other_data.lock().unwrap();
assert_eq!(*data, 100);
});
});
}
- 在这个例子中,第一个线程先修改
other_data
,然后以Release
顺序存储shared_value
。第二个线程以Acquire
顺序加载shared_value
,当加载到非0值时,它能保证看到第一个线程对other_data
的修改,因为Release
和Acquire
一起保证了内存可见性和操作顺序。