我试图理解std::sync::atomic::Atomic*
结构体和原语(如i32
、usize
、bool
)在多线程范围内的一些区别。
第一个问题,另一个线程会看到另一个线程对非原子类型的更改吗?
fn main() {
let mut counter = 0;
std::thread::scope(|scope| {
scope.spawn(|| counter += 1)
});
println!("{counter}");
}
我能确定在另一个线程将这个值写入计数器之后计数器将是1
吗?或者线程可以缓存这个值?如果不能,它是否只能在原子类型下工作?
fn main() {
let counter = AtomicI32::new(0);
std::thread::scope(|scope| {
scope.spawn(|| counter.store(1, Ordering::Release))
});
println!("{}", counter.load(Ordering::Acquire)); // Ordering::Acquire to prevent from reordering previous instructions
}
第二个问题,Ordering
类型是否会影响store
中的值何时在其他线程中可见,或者即使应用了Ordering::Relaxed
,它何时在store
之后可见?例如,相同的代码,但具有Ordering::Relaxed
且没有指令重新排序,是否会在计数器中显示1
?
fn main() {
let counter = AtomicI32::new(0);
std::thread::scope(|scope| {
scope.spawn(|| counter.store(1, Ordering::Relaxed))
});
println!("{}", counter.load(Ordering::Relaxed));
}
我理解原子和非原子写同一个变量之间的区别,我只对另一个线程是否会看到更改感兴趣,即使这些更改不一致。
1条答案
按热度按时间tjrkku2a1#
第一个问题,另一个线程会看到另一个线程对非原子类型的更改吗?
是的。原子变量和非原子变量的区别在于,你可以使用共享引用
&AtomicX
来改变原子变量,而不仅仅是使用可变引用&mut X
。这意味着它们可以在不同的线程中并行地改变。对于原语,编译器将拒绝这种尝试,例如:或者甚至是下面的例子,我们在主线程上使用变量,但是在派生线程被连接之前:
而对于原子,这将工作:
第二个问题,
Ordering
类型是否会影响存储中的值何时在其他线程中可见,还是在存储后立即可见,即使应用了Ordering::Relaxed
?例如,相同的代码,但使用Ordering::Relaxed
且没有指令重新排序,是否会在计数器中显示1?Ordering
不会改变其他线程使用此变量 * 观察到的结果。**因此,您对Release
和Acquire
的使用是错误的。另一方面,由于其他原因,这里的
Relaxed
就足够了。无论使用什么顺序,都可以保证在代码中看到值1,因为
std::thread::scope()
在退出时隐式地联接所有派生的线程,并且联接线程在该线程中所做的一切与联接之后的代码之间形成了 * happens-before * 关系。您可以保证线程中所做的一切(包括存储到counter
)都将在您加入线程后所做的一切(包括读取counter
)之前发生。例如,如果在以下代码中没有联接:
那么不管
Release
和Acquire
的顺序如何,你都不能保证读到更新后的值,可能会发生这种情况,也可能会发生你读到旧值的情况。排序对于创建具有不同变量和代码的happens-before关系非常有用,但这是一个复杂的主题,我推荐阅读this book(由Rust libs团队成员编写)。