原文链接:https://doc.rust-lang.org/nomicon/races.html
数据竞争与竞争条件
安全Rust保证了不存在数据竞争。数据竞争指的是:
- 两个或两个以上的线程并发地访问同一块内存
- 其中一个线程做写操作
- 其中一个线程是非同步(unsynchronized)的
数据竞争导致未定义行为,所以不可能在安全Rust中存在。大多数情况下,Rust的所有权系统就可以避免数据竞争:不可能有可变引用的别名,因此也就不可能有数据竞争。但是内部可变性把这件事弄得复杂了,这也是为什么我们要有Send和Sync(见下)。
但是Rust并不会避免一般竞争条件。
因为要做到这一点其实是不可能的,而且好像也是不必要的。你的硬件是竞争的,操作系统是竞争的,计算机上其他的程序是竞争的,整个世界都是竞争的。任何一个声称可以避免所有竞争条件的系统,即使没有错误,也一定及其难用。
所以,安全Rust出现死锁,或者因为不正确的同步而做出一些奇怪的行为,这些都是可以接受的。显然这样的程序并不是最理想的程序,但Rust也只能帮你到这了。而且,竞争条件自己不能违反Rust的内存安全性。只有配合上其他的非安全代码,竞争条件才有可能破坏内存安全。比如:
use std::thread;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
let data = vec![1, 2, 3, 4];
// 使用Arc,这样即使程序已经执行完毕了,存储AtomicUsize的内存依然存在,
// 其他的线程可以增加它的值。否则Rust不能编译这段代码,因为thread:spawn
// 对生命周期有限制。
let idx = Arc::new(AtomicUsize::new(0));
let other_idx = idx.clone();
// move获得other_idx的所有权,将它移入线程
thread::spawn(move || {
// 可以改变idx,因为它的值是一个原子,不会引起数据竞争
other_idx.fetch_add(10, Ordering::SeqCst);
});
// 用原子中的值做索引。这么做是安全的,因为我们只读取了一次原子的内存,
// 然后将读出的值的拷贝传递给Vec做索引。索引过程可以做正确的边界检查,
// 在执行索引期间这个值也不会发生改变。
// 但是,如果上面的线程在执行这句代码之前增加了这个值,这段代码会panic。
// 这符合竞争条件,因为程序执行得正确与否(panic几乎不可能是正确的)
// 依赖于线程的执行顺序
println!("{}", data[idx.load(Ordering::SeqCst)]);
use std::thread;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
let data = vec![1, 2, 3, 4];
let idx = Arc::new(AtomicUsize::new(0));
let other_idx = idx.clone();
// move获得other_idx的所有权,将它移入线程
thread::spawn(move || {
// 可以改变idx,因为它的值是一个原子,不会引起数据竞争
other_idx.fetch_add(10, Ordering::SeqCst);
});
if idx.load(Ordering::SeqCst) < data.len() {
unsafe {
// 在边界检查之后读取idx的值是不正确的,因为它有可能已经改变了。
// 这是一个竞争条件,而且十分危险,因为我们要使用的get_unchecked是非安全的。
println!("{}", data.get_unchecked(idx.load(Ordering::SeqCst)));
}
}