数据竞争和竞态条件
安全的 Rust 保证没有数据竞争,数据竞争的定义是:
- 两个或多个线程同时访问一个内存位置
- 其中一个或多个线程是写的
- 其中一个或多个是非同步的
数据竞争具有未定义行为,因此在 Safe Rust 中不可能执行。数据竞争主要是通过 Rust 的所有权系统来防止的:不可能别名一个可变引用,所以不可能进行数据竞争。但内部可变性使其更加复杂,这也是我们有 Send 和 Sync Trait 的主要原因(见下个章节更详细的说明)。
然而,Rust 并没有(也无法)阻止更广泛的竞态条件。
在你无法控制调度器的情况下,这在数学上是不可能的,而对于普通的操作系统环境来说你是无法控制调度器的。如果你确实控制了抢占,那么 有可能 防止一般的竞态——这种技术被像 RTIC 这样的框架所使用。然而,实际上拥有对调度的控制是一个非常罕见的情况。
因此,对于一个安全的 Rust 程序来说,在不正确的同步下出现死锁或做一些无意义的事情是完全“正常”的。很明显,这样的程序有问题,但 Rust 只能帮你到这里。不过,Rust 程序中的竞态条件本身并不能违反内存安全;只有与其他不安全的代码结合在一起,竞态条件才能真正违反内存安全。比如说:
#![allow(unused)] fn main() { use std::thread; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; let data = vec![1, 2, 3, 4]; // 使用 Arc,这样即使程序执行完毕,存储 AtomicUsize 的内存依然存在, // 否则由于 thread::spawn 的生命周期限制,Rust 不会为我们编译这段代码 let idx = Arc::new(AtomicUsize::new(0)); let other_idx = idx.clone(); // `move` 捕获了 other_idx 的值,将它移入这个线程 thread::spawn(move || { // 因为这是一个原子变量,不存在数据竞争问题,所以可以修改 other_idx 的值 other_idx.fetch_add(10, Ordering::SeqCst); }); // 因为我们只读取了一次原子的内存,因此用原子中的值做索引是安全的, // 然后将读出的值的拷贝传递给 Vec 做为索引, // 索引过程可以做正确的边界检查,并且在执行索引期间这个值也不会发生改变。 // 但是,如果上面的线程在执行这句代码之前增加了这个值,这段代码会 panic。 // 因为程序的正确执行(panic 几乎不可能是正确的),所以这就是一个 *竞态*, // 其执行结果依赖于线程的执行顺序 println!("{}", data[idx.load(Ordering::SeqCst)]); }
如果我们提前进行边界检查,然后使用未经检查的值不安全地访问数据,我们就可能引起数据竞争:
#![allow(unused)] fn main() { 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 || { // 因为这是一个原子变量,不存在数据竞争问题,所以可以修改 other_idx 的值 other_idx.fetch_add(10, Ordering::SeqCst); }); if idx.load(Ordering::SeqCst) < data.len() { unsafe { // 所以在边界检查之后读取 idx 的值可能是不正确的 // 因为我们这里会 `get_unchecked`, 而这个操作是 `unsafe` 的, // 所以这里就存在着竞态,并且 *非常危险*! println!("{}", data.get_unchecked(idx.load(Ordering::SeqCst))); } } }