基于本文回答

播面 播面

文图音视,全方位拆解八股文
0
评论

RefCell 的运行时借用检查是如何工作的?

知识点图片

RefCell<T> 的运行时借用检查机制主要依赖于内部的一个计数器(borrow counter)以及 RAII(资源获取即初始化)模式的智能指针

简单来说,RefCell 把 Rust 编译器在编译期做的借用检查(Borrow Checker)逻辑,搬到了运行期(Runtime)通过简单的整数加减判断来实现。

下面是详细的工作原理分解:

1. 内部结构

RefCell<T> 的内部大致包含两个部分:

  1. 数据本身:类型为 T 的实例。
  2. 借用状态计数器:通常是一个 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() 时:

  1. 检查:查看计数器是否 < 0(即是否等于 -1)。
  2. 判断
    • 如果为 -1(说明正有人在修改数据),则触发 Panic(或者 try_borrow 返回 Err)。
    • 如果不为 -1(是 0 或正数),则将计数器 +1
  3. 返回:返回一个智能指针 Ref<T>

B. 可变借用 borrow_mut()

当你调用 my_refcell.borrow_mut() 时:

  1. 检查:查看计数器是否 != 0
  2. 判断
    • 如果不为 0(说明已经有人在读,或者有人在写),则触发 Panic
    • 如果为 0(完全空闲),则将计数器设为 -1
  3. 返回:返回一个智能指针 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() }
    }
}

总结

  1. 计数器:用一个整数记录当前的借用状态(读者数量或是否有写者)。
  2. 智能指针守卫borrow()borrow_mut() 返回特定的 Guard 对象。
  3. RAII:利用 Guard 对象的创建(借用开始)和销毁(借用结束)来动态增减计数器。
  4. 非原子操作RefCell 的计数器操作是普通的整数加减,不是原子的(Atomic),因此 RefCell 不是线程安全的(!Sync),只能在单线程中使用。如果需要多线程,必须使用 RwLock(它的原理类似,但使用了原子计数器和线程阻塞机制)。
00:00
00:00