理解可变引用的排他性

本节内容完全属于我个人推理,完全用我个人的理解来解释结论,我不知道官方有没有相关的术语,如果有,盼请告知。另外,如果结论错误,也盼请指正。

不可变引用可以共存,表示允许同时有多个不可变引用来访问数据,这不难理解。

fn main(){
  let x = String::from("junmajinlong");
  let _x1 = &x;
  let _x2 = &x;
  let _x3 = &x;
}

可变引用具有排他性,某数据在某一时刻只允许有一个可变引用,此时不允许有其他任何引用。这看上去似乎这也不难理解。

例如,下面的代码会报错:cannot borrow x as mutable more than once at a time。


#![allow(unused)]
fn main() {
let mut x = String::from("junmajinlong");
let x_mut1 = &mut x;    // (1)
let x_mut2 = &mut x;    // (2)
println!("{}", x_mut1); // (3)
println!("{}", x_mut2); // (4)
}

绝大多数Rust书籍都只是像上面示例一样对【可变引用具有排他性】的结论粗浅地验证一遍。

但真相可能没有这么简单。比如,去掉上面的代码(3)或者同时去掉代码(3)和(4),又或者将代码(3)移到代码(2)之前,得到的代码都是可以正确执行的代码:


#![allow(unused)]
fn main() {
// 可以正确执行
let mut x = String::from("junmajinlong");
let x_mut1 = &mut x;
let x_mut2 = &mut x;
println!("{}", x_mut2);

// 也可以正确执行
let mut x = String::from("junmajinlong");
let x_mut1 = &mut x;
let x_mut2 = &mut x;

// 也可以正确执行
let mut x = String::from("junmajinlong");
let x_mut1 = &mut x;
println!("{}", x_mut1);
let x_mut2 = &mut x;
println!("{}", x_mut2);
}

从上面的测试来看,同一份数据的多个可变引用是可以共存的,可变引用具有排他性的结论不攻自破。实际上,可变引用具有排他性的【排他性】,其含义体现在更深层次。

可以将可变引用看作是对内存数据的一把独占锁。在当前作用域内,从第一次使用可变引用开始创建这把独占锁,之后无论使用原始变量、可变引用还是不可变引用都会抢占这把独占锁,以保证只有一方可以访问数据,每次抢得独占锁后,都会将之前所有引用变量给锁住(原始变量依然可用),使它们变成不可用状态。当离开当前作用域时,当前作用域内的所有独占锁都被释放。

因此,可变引用是抢占且排他的,将其称为抢占式独占锁更为合适。

换个角度来理解,自从第一次使用可变引用导致独占锁出现后,可以随时使用原始变量、可变引用或不可变引用来抢独占锁,但抢锁后以前的引用变量就不能再用,且当前持有的锁也可以随时被抢走。一切都由程序员控制,程序员可以在任意代码位置通过原始变量或引用来抢锁。

下面通过示例来分析上述规则。

fn main(){
  let mut a = String::from("junmajinlong");

  // 创建两个不可变引用,不可变引用可以共存
  // 此时还没有独占锁
  let a_non_ref1 = &a;
  let a_non_ref2 = &a;
  // 可直接使用不可变引用
  println!("{}", a_non_ref1);
  println!("{}", a_non_ref2);

  // 第一次使用可变引用,将出现独占锁,a_ref1拥有独占锁
  let a_ref1 = &mut a;
  // 抢占独占锁后,前面两个不可变引用变量将不能使用
  // 因此下面两行代码报错
  //   println!("{}", a_non_ref1);
  //   println!("{}", a_non_ref2);

  // 再次使用不可变引用,a_non_ref3将获得独占锁
  let a_non_ref3 = &a;
  // 抢占独占锁后,前面所有引用变量都不能使用
  // 因此下面代码会报错
  //   println!("{}", a_ref1);
  //   println!("{}", a_non_ref1);

  // 再次使用可变引用,a_ref2将获得独占锁
  // 抢占后前面所有该数据的引用都不可用
  let a_ref2 = &mut a;
  // 但a_ref2是可用的
  println!("{}", a_ref2);

  // 任何时候使用原始变量a,也会抢占独占锁
  // 原始变量抢得独占锁后,前面所有引用变量将不能使用
  println!("{}", a);
  // 因此下面的代码会报错
  //   println!("{}", a_ref2);
}

理解上面的分析后,再分析代码是否错误以及为什么将非常轻松。

例如,下面第一段代码为什么不报错,而第二段代码是错误的:

fn main(){
  let mut x = String::from("junmajinlong");

  // (1).下面这段代码是正确的
  let x1 = &mut x;     // 独占锁出现,x1拥有独占锁
  println!("{}", x1); // x1是可用的变量
  let x2 = &mut x;    // x2抢占独占锁,x1不可用
  println!("{}", x2); // x2是可用的变量

  // (2).下面这段代码是错误的
  let x3 = &mut x;    // x3抢占独占锁
  ff(&x);  // &x抢占独占锁,参数s获得锁,使得x3不可用
  println!("{}", x3); // 使用了x3,导致报错,注释本行将正确
}

fn ff(s: &String){
  println!("{}", s);
}

再看下面这段代码:

fn main(){
  let mut x = 33;
  let y = &mut x; // y获得独占锁
  x = *y + 1;     // 使用y获取数据后,x重新抢得独占锁
                  // 赋值之后,x有效,y将失效
  println!("{}", x);     // 正确
  // println!("{}", y);  // 错误
}