Rust 中的共享引用与独占引用详解

本质上,Rust 中 &T&mut T 的区别不仅仅是“不可变 vs. 可变”,更根本的是“共享 vs. 独占”访问。这是理解像 Mutex 这样的类型的关键。

1. 正确的思维模型:共享 vs. 独占

&T(共享引用)

  • 含义:授予共享访问权限。这意味着你可以同时拥有多个指向同一数据的 &T 引用,甚至可以跨线程。
  • 保证:Rust 编译器保证,只要存在任何共享引用,就不能被独占借用(因此,不能通过“普通”的 &mut T 进行修改)。这就是为什么你不能在一个被共享的变量上调用接受 &mut self 的方法。
  • 注意:它并不严格等同于“不可变”。它意味着“不允许独占访问”,并不意味着底层数据永远不会改变。

&mut T(独占引用)

  • 含义:授予独占访问权限。
  • 保证:Rust 编译器保证,如果你拥有一个 &mut T,你就是当前作用域中唯一拥有该数据引用的人。此时不存在其他 &T&mut T 引用。
  • 结果:由于访问是独占的,修改数据是绝对安全的。

2. Mutex<T> 的作用:运行时独占性

现在,让我们将其应用到 Mutex<T> 上。正如你的笔记所指出的,MutexRefCell<T> 的线程安全版本,是内部可变性的典型例子。

2.1 共享 Mutex

当你将 Mutex<T> 包裹在 Arc<T> 中时,你创建了多个对 Mutex<T> 的共享引用(&Mutex<T>),可以传递给不同的线程。这完全没问题,因为 Mutex 本身就是为了被共享而设计的。

// `counter` 是一个 Arc,提供对 Mutex 的共享所有权。
// 每个线程获得一个克隆,本质上是一个共享引用。
let counter = Arc::new(Mutex::new(0));

2.2 lock() 方法

Mutex::lock 的签名很有启发性:

pub fn lock(&self) -> LockResult<MutexGuard<'_, T>>

注意它接受 &self —— 即对 Mutex 的共享引用。如果我们只用“不可变”这个思维模型,似乎不可能允许这个方法进行修改。

2.3 运行时实现独占性

这就是 Mutex 的魔力。它在运行时而不是编译时强制执行 Rust 的独占性规则(&mut T)。

  • 当你调用 .lock() 时,你是在请求 Mutex:“我可以获得对你保护的数据的独占引用吗?”
  • Mutex 充当守门人。如果没有其他线程持有锁,它会说“可以”,并给你一个 MutexGuard
  • 如果有其他线程持有锁,你的线程会等待(阻塞),直到锁被释放。
  • MutexGuard<T> 是一个智能指针,可以解引用为 &mut T。它是你临时独占访问数据 T 的具体体现。
// 1. 我们从对 Mutex 的共享引用(`&counter_clone`)开始。
// 2. .lock() 接受这个共享引用……
// 3. 并返回一个 guard。
let mut num = counter_clone.lock().unwrap();

// 4. 这个 guard(`num`)给了我们对内部数据的独占引用。
//    现在我们可以安全地修改它。
*num += 1;

// 5. 当 `num`(MutexGuard)离开作用域时,锁被释放,
//    我们的独占访问结束。

总结

  • 你可以在多个线程间拥有许多对 Mutex<T> 的共享引用(&)。
  • Mutex<T> 的作用是让你能够安全、临时地将你的共享引用(&Mutex<T>)升级为对内部数据的独占引用(&mut T)。
  • 它通过运行时的锁机制强制执行“一次只能有一个独占引用”的规则,使其成为线程安全的内部可变性工具。

这种“共享 vs. 独占”的模型很好地解释了为什么你可以在一个共享值上调用 .lock(),最终获得对其保护数据的可变访问能力。