原文链接:https://doc.rust-lang.org/nomicon/races.html

数据竞争与竞争条件

安全Rust保证了不存在数据竞争。数据竞争指的是:

  • 两个或两个以上的线程并发地访问同一块内存
  • 其中一个线程做写操作
  • 其中一个线程是非同步(unsynchronized)的

数据竞争导致未定义行为,所以不可能在安全Rust中存在。大多数情况下,Rust的所有权系统就可以避免数据竞争:不可能有可变引用的别名,因此也就不可能有数据竞争。但是内部可变性把这件事弄得复杂了,这也是为什么我们要有Send和Sync(见下)。

但是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. // 其他的线程可以增加它的值。否则Rust不能编译这段代码,因为thread:spawn
  7. // 对生命周期有限制。
  8. let idx = Arc::new(AtomicUsize::new(0));
  9. let other_idx = idx.clone();
  10. // move获得other_idx的所有权,将它移入线程
  11. thread::spawn(move || {
  12. // 可以改变idx,因为它的值是一个原子,不会引起数据竞争
  13. other_idx.fetch_add(10, Ordering::SeqCst);
  14. });
  15. // 用原子中的值做索引。这么做是安全的,因为我们只读取了一次原子的内存,
  16. // 然后将读出的值的拷贝传递给Vec做索引。索引过程可以做正确的边界检查,
  17. // 在执行索引期间这个值也不会发生改变。
  18. // 但是,如果上面的线程在执行这句代码之前增加了这个值,这段代码会panic。
  19. // 这符合竞争条件,因为程序执行得正确与否(panic几乎不可能是正确的)
  20. // 依赖于线程的执行顺序
  21. 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. // 可以改变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是非安全的。
  16. println!("{}", data.get_unchecked(idx.load(Ordering::SeqCst)));
  17. }
  18. }