理解可变引用的排他性

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

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

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);

  // 这里println!中使用的是a的不可变引用&a,
  // 但不可变引用也会抢占独占锁,前面所有引用变量将不能使用
  println!("{}", a);
  // 因此下面的代码会报错
  //   println!("{}", a_ref2);
  
  // 任何时候使用原始变量a,也会抢占独占锁
  a = String::from("junmajinlong");
}

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

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

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 = Box::new(42); // 1

  // 创建x的不可变引用
  let mut z = &x; // 2

  // 在考虑引用检查问题和生命周期问题时,循环结构for {} 和多个独立的大括号 {} 是等价的
  for i in 0..100 {
    // 使用z的不可变引用
    println!("{}", z); // 3

    // 抢占x的独占锁,使得z不再可用
    // 第一轮循环时创建x的独占锁
    x = Box::new(i); // 4
    // 因此下面的代码会报错
    // println!("{}", z);

    // 虽然z不可用,但z自身可以被重新赋值,重新赋值将丢弃z之前对x的引用,
    // 注意这里使用了x的不可变引用,它会抢占x的独占锁,
    // 虽然这里z重新引用了x,但和赋值之前引用的x已经不一样,它是一个新的引用,
    // 并且z在这里抢占到了新的x独占锁,而赋值之前的x独占锁已经被代码行4抢占
    z = &x; // 5
  }
}

如果注释上面的代码行5:z = &x,编译器将报错,如此修改后,上面的循环等价于:

fn main() {
  let mut x = Box::new(42); // 1
  let mut z = &x; // 2
  {
    println!("{}", z); // 3
    x = Box::new(0); // 4 创建x的独占锁,z不再可用
  }
  {
    println!("{}", z); // 代码行4抢占独占锁,z不再可用,报错
    x = Box::new(1);
  }
  ...
}

再看下面这段代码:

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

如果从位置表达式和值的角度来理解引用,会更直观更容易理解。在通过位置和值理解内存模型中说过,位置具有一些状态标记,其中之一就是该位置当前是否正在被引用以及如何被引用的状态标记。

对某个位置每建立一次引用就记录一次,如果是建立共享引用,则简单判断即可,但对该位置进行可变引用之后,从此刻开始的任意时刻,这个位置将只能存在单一使用者,使用者可以是原始变量,可以是新的可变引用或不可变引用,使用者可以随时更换,但保证任意时刻只能有一个使用者