数据竞争和竞态条件
安全 Rust 保证没有数据竞争,数据竞争定义为
- 两个或多个线程并发访问内存的同一位置
- 其中一个或多个是写操作
- 其中一个或多个是未同步的
数据竞争具有未定义行为,因此在安全 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 so that the memory the AtomicUsize is stored in still exists for // the other thread to increment, even if we completely finish executing // before it. Rust won't compile the program without it, because of the // lifetime requirements of thread::spawn! let idx = Arc::new(AtomicUsize::new(0)); let other_idx = idx.clone(); // `move` captures other_idx by-value, moving it into this thread thread::spawn(move || { // It's ok to mutate idx because this value // is an atomic, so it can't cause a Data Race. other_idx.fetch_add(10, Ordering::SeqCst); }); // Index with the value loaded from the atomic. This is safe because we // read the atomic memory only once, and then pass a copy of that value // to the Vec's indexing implementation. This indexing will be correctly // bounds checked, and there's no chance of the value getting changed // in the middle. However our program may panic if the thread we spawned // managed to increment before this ran. A race condition because correct // program execution (panicking is rarely correct) depends on order of // thread execution. 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` captures other_idx by-value, moving it into this thread thread::spawn(move || { // It's ok to mutate idx because this value // is an atomic, so it can't cause a Data Race. other_idx.fetch_add(10, Ordering::SeqCst); }); if idx.load(Ordering::SeqCst) < data.len() { unsafe { // Incorrectly loading the idx after we did the bounds check. // It could have changed. This is a race condition, *and dangerous* // because we decided to do `get_unchecked`, which is `unsafe`. println!("{}", data.get_unchecked(idx.load(Ordering::SeqCst))); } } }