内部可变性
我们了解了 Rust 严苛的所有权与借用规则——数据要么是独占可变的 (&mut),要么是共享不可变的 (&). 但对于一些全局访问或者需要异步处理的资源,在保持 Rust 语义检查的情况下,我们需要引入内部可变性 (Interior Mutability) 这个概念.
通常,当你持有一个不可变引用 &T 时,Rust 禁止你修改它指向的数据. 这是外部可变性 (Inherited Mutability) 变量的可变性由它的引用类型 (可变引用或不可变引用) 决定.
内部可变性允许你在拥有不可变引用时修改数据. 这听起来不太行,但它是安全的,因为我们将借用规则的检查从编译期 (Compile time) 推迟到了运行时 (Runtime),或者通过原子操作保证了安全性。
所有内部可变性类型的底层核心都是 UnsafeCell<T>. 它是 Rust 语言中唯一被编译器特殊对待的类型,它明确告诉编译器,“即使你持有这个类型的共享引用,其内部数据也可能被修改,请不要做激进的优化”.
如果你有一个引用 &T,通常 Rust 编译器会基于 &T 指向不可变数据这一认知进行优化. 通过别名或通过将 &T 转换为 &mut T 来改变该数据被视为未定义行为. UnsafeCell 取消了 &T 的不可变性保证,共享引用 &UnsafeCell 可能指向正在被修改的数据. 这被称为内部可变性.
所有我们使用的安全封装,诸如 Cell, RefCell, Mutex 内部都包裹着 UnsafeCell.
单线程内的动态借用
Section titled “单线程内的动态借用”在单线程环境下,如果编译器无法静态确认借用安全性,我们可以请求运行时检查.
Cell<T> 与 Copy trait
Section titled “Cell<T> 与 Copy trait”Cell<T> 适用于实现了 Copy trait 的简单类型.
Cell<T> 不返回数据的引用,它没有 get(&self) -> &T 这种方法,仅提供值.
- 它通过
set()将新值移动 (Move/Copy) 进去覆盖旧值. - 它通过
get()将内部值拷贝 (Copy) 出来.
因为永远拿不到内部数据的引用 &T,所以永远不会发生“同时拥有可变引用和不可变引用”的情况. 由此,指针别名问题被物理隔离了.
RefCell<T> 运行时借用检查
Section titled “RefCell<T> 运行时借用检查”对于复杂类型助如 Vec 和 String,使用引用 (或者说指针) 可以减少频繁拷贝的代价.
RefCell<T> 与 Cell<T> 几乎是一样的,正如其名,主要差异在于 RefCell<T> 提供引用,而 Cell<T> 提供值,另外 RefCell<T> 有 panic 的风险.
RefCell 在内部维护了一个运行时的借用计数器 (borrow counter). 它把编译器的借用检查逻辑搬到了程序运行时执行. RefCell 通过 borrow 或 borrow_mut 等方法来访问值.
borrow(): 借用计数 +1. 如果发现当前已有&mut借用,则 Panic.borrow_mut(): 检查计数是否为 0,如果不为 0,说明已有&或&mut,则 Panic;否则标记为独占.
并发多线程与其同步原语
Section titled “并发多线程与其同步原语”OS 内核是天然的多线程程序,我们需要线程安全的内部可变性. 这里涉及两个关键的 Trait:
Send / Sync
Section titled “Send / Sync”Rust 的 Mutex<T> 互斥锁作为数据的容器,用户无法直接访问 T,而是调用 .lock() 来阻塞当前线程直到获取锁,.lock() 返回一个 MutexGuard<T> 智能指针. 用户通过 MutexGuard 访问 T,当 MutexGuard Drop 时,自动释放锁.
Rc (Reference Count) 和 Arc (Atomic Reference Count) 是关于引用计数智能指针. 如果说 Mutex 致力于解决谁修改的问题,那么 Arc 在于解决谁持有的问题.
在单线程中,我们可以使用 Rc<T> 来让多个变量拥有同一个数据. 但在多线程环境中,Rc 是不安全的,因为它的引用计数操作不是原子的. 如果两个线程同时克隆 Rc,会导致计数错误,Arc 是更加线程安全的版本.
Arc<T> 目的在于实现共享状态. Arc 使用 CPU 的原子指令来增减引用计数,保证了线程安全. 原子操作通常比普通的内存读写要慢.
在内核开发中,你几乎总会看到 Arc 和 Mutex 成对出现为 Arc<Mutex<T>>. 这是 Rust 处理并发共享状态的标准模式:
Arc提供多所有权,让多个线程都能持有一个指向数据的指针.Mutex提供内部可变性,确保同一时刻只有一个线程能修改数据.
主线程依然持有 counter,可以读取最终结果.
全局变量与 Lazy Evaluation
Section titled “全局变量与 Lazy Evaluation”static 意味着一个变量有固定的内存地址,如果它有初始值,它会被硬编码进 .data 段,
如果它未初始化或为零,它会在 .bss 段占据一个位置.
Rust 要求 static 变量必须在编译期初始化. 但这对于需要堆分配的类型诸如 Vec, Mutex<T>)往往不可能,所以我们引入 lazy_static! 或者 once_cell 来将初始化推迟到运行时.
lazy_static! 在于,虽然变量声明为 static,但其实际的初始化逻辑被延迟到了第一次被访问的时候执行. 其实现内部使用原子状态机 (如 std::sync::Once) 确保初始化函数只被执行一次,而且是线程安全的.
在 Rust 中,你不能简单地声明一个全局可变变量. 你需要组合使用这些工具来构建安全的全局状态:
RefCell<T>提供引用,而Cell<T>提供值,解决数据修改的问题.Arc<T>用引用计数提供多线程间的共享所有权.Mutex<T>提供跨线程的内部可变性以及访问控制.lazy_static!解决全局变量的运行时初始化问题.