原文链接:https://doc.rust-lang.org/nomicon/races.html
数据竞争与竞争条件
安全Rust保证了不存在数据竞争。数据竞争指的是:
- 两个或两个以上的线程并发地访问同一块内存
- 其中一个线程做写操作
- 其中一个线程是非同步(unsynchronized)的
数据竞争导致未定义行为,所以不可能在安全Rust中存在。大多数情况下,Rust的所有权系统就可以避免数据竞争:不可能有可变引用的别名,因此也就不可能有数据竞争。但是内部可变性把这件事弄得复杂了,这也是为什么我们要有Send和Sync(见下)。
但是Rust并不会避免一般竞争条件。
因为要做到这一点其实是不可能的,而且好像也是不必要的。你的硬件是竞争的,操作系统是竞争的,计算机上其他的程序是竞争的,整个世界都是竞争的。任何一个声称可以避免所有竞争条件的系统,即使没有错误,也一定及其难用。
所以,安全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的内存依然存在, // 其他的线程可以增加它的值。否则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)]); }
#![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 || { // 可以改变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))); } } }