数据竞争和竞态条件

安全的 Rust 保证没有数据竞争,数据竞争的定义是:

  • 两个或多个线程同时访问一个内存位置
  • 其中一个或多个线程是写的
  • 其中一个或多个是非同步的

数据竞争具有未定义行为,因此在 Safe Rust 中不可能执行。数据竞争主要是通过 Rust 的所有权系统来防止的:不可能别名一个可变引用,所以不可能进行数据竞争。但内部可变性使其更加复杂,这也是我们有 Send 和 Sync Trait 的主要原因(见下文)。

然而,Rust 并没有(也无法)阻止更广泛的竞态条件。

这从根本上说是不可能的,而且说实话也是不可取的。你的硬件很糟糕,你的操作系统很糟糕,你电脑上的其他程序也很糟糕,而这一切运行的世界也很糟糕。任何能够真正声称防止所有竞态条件的系统,如果不是不正确的话,使用起来也是非常糟糕的。

因此,对于一个安全的 Rust 程序来说,在不正确的同步下出现死锁或做一些无意义的事情是完全“正常”的。很明显,这样的程序有问题,但 Rust 只能帮你到这里。不过,Rust 程序中的竞态条件本身并不能违反内存安全;只有与其他不安全的代码结合在一起,竞态条件才能真正违反内存安全。比如说:

  1. use std::thread;
  2. use std::sync::atomic::{AtomicUsize, Ordering};
  3. use std::sync::Arc;
  4. let data = vec![1, 2, 3, 4];
  5. // 使用 Arc,这样即使程序执行完毕,存储 AtomicUsize 的内存依然存在,
  6. // 否则由于 thread::spawn 的生命周期限制,Rust 不会为我们编译这段代码
  7. let idx = Arc::new(AtomicUsize::new(0));
  8. let other_idx = idx.clone();
  9. // `move` 捕获了 other_idx 的值,将它移入这个线程
  10. thread::spawn(move || {
  11. // 因为这是一个原子变量,不存在数据竞争问题,所以可以修改 other_idx 的值
  12. other_idx.fetch_add(10, Ordering::SeqCst);
  13. });
  14. // 因为我们只读取了一次原子的内存,因此用原子中的值做索引是安全的,
  15. // 然后将读出的值的拷贝传递给 Vec 做为索引,
  16. // 索引过程可以做正确的边界检查,并且在执行索引期间这个值也不会发生改变。
  17. // 但是,如果上面的线程在执行这句代码之前增加了这个值,这段代码会 panic。
  18. // 因为程序的正确执行(panic 几乎不可能是正确的),所以这就是一个 *竞态*,
  19. // 其执行结果依赖于线程的执行顺序
  20. println!("{}", data[idx.load(Ordering::SeqCst)]);
  1. use std::thread;
  2. use std::sync::atomic::{AtomicUsize, Ordering};
  3. use std::sync::Arc;
  4. let data = vec![1, 2, 3, 4];
  5. let idx = Arc::new(AtomicUsize::new(0));
  6. let other_idx = idx.clone();
  7. // `move` 捕获了 other_idx 值,将它移入这个线程
  8. thread::spawn(move || {
  9. // 因为这是一个原子变量,不存在数据竞争问题,所以可以修改 other_idx 的值
  10. other_idx.fetch_add(10, Ordering::SeqCst);
  11. });
  12. if idx.load(Ordering::SeqCst) < data.len() {
  13. unsafe {
  14. // 所以在边界检查之后读取 idx 的值可能是不正确的
  15. // 因为我们这里会 `get_unchecked`, 而这个操作是 `unsafe` 的,
  16. // 所以这里就存在着竞态,并且 *非常危险*!
  17. println!("{}", data.get_unchecked(idx.load(Ordering::SeqCst)));
  18. }
  19. }