8.1 竞态
数据竞争与竞态条件
2 分钟阅读
译文 · 基于 The Rustonomicon
竞态
Safe Rust 保证不存在数据竞争(data race),其定义为:
- 两个或多个线程并发访问同一内存位置
- 其中至少一个是写
- 其中至少一个是未同步的
数据竞争具有未定义行为,因此在 Safe Rust 中不可能发生。数据竞争主要通过 Rust 的所有权系统 alone 来防止:不可能别名可变引用,因此不可能发生数据竞争。内部可变性(interior mutability)使情况更复杂,这也是我们有 Send 和 Sync trait 的重要原因(下一节详述)。
然而 Rust 并不阻止一般的竞态条件(race condition)。
在你无法控制调度器的场景下,这在数学上不可能——对普通 OS 环境确实如此。若你控制抢占,可以阻止一般竞态——RTIC 等框架就采用这种技术。然而实际上控制调度非常罕见。
因此,Rust 因错误同步而死锁或做出荒谬行为,被认为仍是「safe」的:这称为一般竞态条件或资源竞态。显然这样的程序并不好,但 Rust 当然无法阻止所有逻辑错误。
无论如何,竞态条件 alone 无法在 Rust 程序中违反内存安全。只有与其他 unsafe 代码结合时,竞态条件才可能真正违反内存安全。例如,一个正确的程序如下:
use std::thread;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
let data = vec![1, 2, 3, 4];
// 使用 Arc,以便 AtomicUsize 所在内存在另一线程递增时仍存在,
// 即使我们在它之前完全执行完毕。没有 Arc Rust 不会编译通过,
// 因为 thread::spawn 的 lifetime 要求!
let idx = Arc::new(AtomicUsize::new(0));
let other_idx = idx.clone();
// `move` 按值捕获 other_idx,移入此线程
thread::spawn(move || {
// 可以 mutate idx,因为该值是 atomic,
// 不会造成 Data Race。
other_idx.fetch_add(10, Ordering::SeqCst);
});
// 用从 atomic 加载的值索引。这是安全的,因为我们只读一次 atomic 内存,
// 然后把该值的副本传给 Vec 的索引实现。此索引会正确做边界检查,
// 且值不会在中间被改变。然而若我们 spawn 的线程在此运行前已递增,
// 程序可能 panic。这是竞态条件,因为正确执行(panic 很少是正确行为)
// 取决于线程执行顺序。
println!("{}", data[idx.load(Ordering::SeqCst)]);
若我们提前做边界检查,然后用未检查的值 unsafe 访问数据,竞态条件可能违反内存安全:
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 || {
// 可以 mutate idx,因为该值是 atomic,
// 不会造成 Data Race。
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)));
}
}