面试题答案
一键面试内存顺序对多线程累加统计功能准确性的影响
-
SeqCst(顺序一致性)
- 程序行为:SeqCst 提供了最强的内存顺序保证。所有线程对原子操作的顺序都达成一致,就好像所有操作是按顺序一个接一个执行的。这意味着在多线程环境下,每个线程看到的原子操作顺序都是一样的。
- 准确性:使用 SeqCst 能确保累加统计功能的准确性,因为所有线程对原子变量的读写操作顺序是一致的,不会出现数据竞争导致的不一致情况。
-
Acquire
- 程序行为:Acquire 内存顺序保证在读取原子变量之后的所有内存操作不会被重排到读取操作之前。当一个线程以 Acquire 顺序读取原子变量时,它能看到在其他线程中以 Release 顺序写入该变量之前的所有内存写入操作。
- 准确性:在累加统计功能中,如果读取操作使用 Acquire 顺序,它能确保读取到最新的值,但可能无法保证所有线程看到的操作顺序完全一致,可能会出现轻微的数据不一致情况,但在大多数情况下能保证最终一致性。
-
Release
- 程序行为:Release 内存顺序保证在写入原子变量之前的所有内存操作不会被重排到写入操作之后。当一个线程以 Release 顺序写入原子变量时,它能确保在该写入操作之前的所有内存写入操作对其他以 Acquire 顺序读取该变量的线程可见。
- 准确性:在累加统计功能中,写入操作使用 Release 顺序,结合读取操作的 Acquire 顺序,能保证数据的可见性和一定程度的一致性,但同样不能保证像 SeqCst 那样严格的顺序一致性。
代码示例
use std::sync::atomic::{AtomicUsize, Ordering};
use std::thread;
fn main() {
let counter = AtomicUsize::new(0);
let mut handles = vec![];
for _ in 0..10 {
let counter_clone = counter.clone();
let handle = thread::spawn(move || {
for _ in 0..1000 {
counter_clone.fetch_add(1, Ordering::SeqCst);
}
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Final counter value: {}", counter.load(Ordering::SeqCst));
}
上述代码使用 AtomicUsize
和 fetch_add
方法以 SeqCst
内存顺序进行累加操作。如果要展示其他内存顺序,可以将 Ordering::SeqCst
替换为 Ordering::Acquire
或 Ordering::Release
进行观察。例如,将写入操作改为 Release
,读取操作改为 Acquire
:
use std::sync::atomic::{AtomicUsize, Ordering};
use std::thread;
fn main() {
let counter = AtomicUsize::new(0);
let mut handles = vec![];
for _ in 0..10 {
let counter_clone = counter.clone();
let handle = thread::spawn(move || {
for _ in 0..1000 {
counter_clone.fetch_add(1, Ordering::Release);
}
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Final counter value: {}", counter.load(Ordering::Acquire));
}
在实际运行中,使用 Acquire
和 Release
顺序可能会导致结果与 SeqCst
顺序略有不同,因为它们的内存顺序保证相对较弱,但在大多数情况下,累加结果应该是接近预期值的。