RefCell 的运行时借用检查是如何工作的?
RefCell<T> 的运行时借用检查机制主要依赖于内部的一个计数器(borrow counter)以及 RAII(资源获取即初始化)模式的智能指针。
简单来说,RefCell 把 Rust 编译器在编译期做的借用检查(Borrow Checker)逻辑,搬到了运行期(Runtime)通过简单的整数加减判断来实现。
下面是详细的工作原理分解:
1. 内部结构
RefCell<T> 的内部大致包含两个部分:
- 数据本身:类型为
T的实例。 - 借用状态计数器:通常是一个
isize(或类似的整数类型)。
plaintext
// 伪代码示意
struct RefCell<T> {
value: UnsafeCell<T>, // 用于存放数据,提供内部可变性
borrow: Cell<isize>, // 借用计数器
}
2. 计数器的状态逻辑
这个 borrow 计数器的数值代表了当前的借用状态:
0(Unused): 当前没有任何借用。既没有读,也没有写。正数 (+N)(Shared): 当前有N个不可变借用(active immutable borrows)。-1(Exclusive): 当前有且仅有 1 个可变借用(active mutable borrow)。
3. 借用流程(Borrowing)
当你调用 borrow() 或 borrow_mut() 时,RefCell 会检查计数器的值。
A. 不可变借用 borrow()
当你调用 my_refcell.borrow() 时:
- 检查:查看计数器是否
< 0(即是否等于 -1)。 - 判断:
- 如果为
-1(说明正有人在修改数据),则触发 Panic(或者try_borrow返回 Err)。 - 如果不为
-1(是 0 或正数),则将计数器 +1。
- 如果为
- 返回:返回一个智能指针
Ref<T>。
B. 可变借用 borrow_mut()
当你调用 my_refcell.borrow_mut() 时:
- 检查:查看计数器是否
!= 0。 - 判断:
- 如果不为
0(说明已经有人在读,或者有人在写),则触发 Panic。 - 如果为
0(完全空闲),则将计数器设为 -1。
- 如果不为
- 返回:返回一个智能指针
RefMut<T>。
4. 归还流程(Dropping)—— 关键所在
RefCell 不会直接返回 &T 或 &mut T,而是返回智能指针 Ref<T> 和 RefMut<T>。这是运行时检查能闭环的关键。
这两个智能指针实现了 Drop trait。当它们离开作用域被销毁时,会自动更新 RefCell 内部的计数器。
Ref<T>的 Drop:- 将
RefCell的计数器 -1。 - 如果计数器从 1 变回 0,说明没有读者了。
- 将
RefMut<T>的 Drop:- 将
RefCell的计数器从 -1 重置回 0。 - 说明写者已经释放了锁。
- 将
5. 简化的代码实现演示
为了更直观地理解,我们可以写一个简化版的 RefCell 逻辑:
plaintext
use std::cell::UnsafeCell;
use std::ops::{Deref, DerefMut};
struct MyRefCell<T> {
value: UnsafeCell<T>,
borrow_state: std::cell::Cell<isize>, // 0: 空闲, >0: 读者数量, -1: 写者
}
impl<T> MyRefCell<T> {
fn new(value: T) -> Self {
Self {
value: UnsafeCell::new(value),
borrow_state: std::cell::Cell::new(0),
}
}
fn borrow(&self) -> MyRef<T> {
// 运行时检查:如果有写者,Panic
if self.borrow_state.get() < 0 {
panic!("Already mutably borrowed!");
}
// 计数器 +1
let new_borrow = self.borrow_state.get() + 1;
self.borrow_state.set(new_borrow);
MyRef { parent: self }
}
fn borrow_mut(&self) -> MyRefMut<T> {
// 运行时检查:如果有读者或写者,Panic
if self.borrow_state.get() != 0 {
panic!("Already borrowed!");
}
// 设置为 -1
self.borrow_state.set(-1);
MyRefMut { parent: self }
}
}
// 智能指针:不可变引用
struct MyRef<'a, T> {
parent: &'a MyRefCell<T>,
}
impl<'a, T> Drop for MyRef<'a, T> {
fn drop(&mut self) {
// 离开作用域时,计数器 -1
let old_borrow = self.parent.borrow_state.get();
self.parent.borrow_state.set(old_borrow - 1);
}
}
impl<'a, T> Deref for MyRef<'a, T> {
type Target = T;
fn deref(&self) -> &T {
unsafe { &*self.parent.value.get() }
}
}
// 智能指针:可变引用
struct MyRefMut<'a, T> {
parent: &'a MyRefCell<T>,
}
impl<'a, T> Drop for MyRefMut<'a, T> {
fn drop(&mut self) {
// 离开作用域时,重置为 0
self.parent.borrow_state.set(0);
}
}
impl<'a, T> Deref for MyRefMut<'a, T> {
type Target = T;
fn deref(&self) -> &T {
unsafe { &*self.parent.value.get() }
}
}
impl<'a, T> DerefMut for MyRefMut<'a, T> {
fn deref_mut(&mut self) -> &mut T {
unsafe { &mut *self.parent.value.get() }
}
}
总结
- 计数器:用一个整数记录当前的借用状态(读者数量或是否有写者)。
- 智能指针守卫:
borrow()和borrow_mut()返回特定的 Guard 对象。 - RAII:利用 Guard 对象的创建(借用开始)和销毁(借用结束)来动态增减计数器。
- 非原子操作:
RefCell的计数器操作是普通的整数加减,不是原子的(Atomic),因此RefCell不是线程安全的(!Sync),只能在单线程中使用。如果需要多线程,必须使用RwLock(它的原理类似,但使用了原子计数器和线程阻塞机制)。