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> 上。正如你的笔记所指出的,Mutex 是 RefCell<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(),最终获得对其保护数据的可变访问能力。