数据竞态和竞态条件

安全的 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)));
    }
}
}