Rc, RefCell, Arc 和 Mutex 的区别与组合
我们一步步来,从单线程世界走向多线程世界。
核心概念:两个维度
想象一个坐标系,你需要根据两个维度来选择合适的工具:
- 所有权维度:这个数据是唯一所有权还是共享所有权?
- 可变性维度:我们是需要外部可变性 (
&mut T) 还是内部可变性(通过&T修改内部)?
第一站:单线程世界 (std::rc 和 std::cell)
在单线程环境下,我们不需要担心数据竞争,但仍然需要处理复杂的所有权和可变性问题。
1. Rc<T> (Reference Counted) - “共享图书馆借书卡”
- 解决什么问题? 当你希望一块数据(比如一个图中的节点)在单线程内有多个所有者时使用。普通的 Rust 所有权规则规定一个值只能有一个所有者。
- 工作原理:
Rc<T>在堆上分配一个值T,并附带一个“引用计数器”。每次克隆 Rc(Rc::clone(&rc)),你得到的不是T的深拷贝,而是一个指向相同数据的新指针,同时计数器加一。当一个 Rc 指针被销毁(离开作用域),计数器减一。当计数器归零时,数据T会被自动清理。 - 核心特点:
- 共享所有权。
- 只能用于单线程。它的引用计数器是非原子的,在多线程下递增/递减会产生数据竞争。
Rc<T>本身是不可变的,你无法通过它获得&mut T。
示例
use std::rc::Rc;
// 想象一个链表,我们希望两个列表能共享一部分节点
let common_node = Rc::new(5);
let list_a = Rc::clone(&common_node);
let list_b = Rc::clone(&common_node);
println!("Reference count: {}", Rc::strong_count(&common_node)); // 输出: Reference count: 3
2. RefCell<T> - “带锁的日记本”
- 解决什么问题? 当你有一个不可变的值(比如
&T),但你需要修改它内部的数据时。这就是内部可变性。 - 工作原理:
RefCell<T>将 Rust 编译器的静态借用检查,转移到了运行时检查。borrow():在运行时请求一个不可变借用(&T)。可以同时有多个。borrow_mut():在运行时请求一个可变借用(&mut T)。只能有一个。- 如果违反了借用规则(比如在已有可变借用的情况下又请求一个),程序会直接
panic!。
- 核心特点:
- 内部可变性。
- 只能用于单线程。它的借用检查逻辑是非线程安全的。
- 它不共享所有权,它自己就是数据的所有者。
示例
use std::cell::RefCell;
// config 是一个不可变绑定,但我们想修改它内部的值
let config = RefCell::new(String::from("initial"));
// 通过 .borrow_mut() 获取可变借用
*config.borrow_mut() = String::from("modified");
println!("{}", config.borrow()); // 输出: modified
组合技: Rc<RefCell<T>> - “多人共享的可编辑在线文档”
这是单线程中最强大的组合。Rc 提供了共享所有权,RefCell 提供了内部可变性。
- 解决什么问题? 你希望一块数据被多个所有者共享,并且任何一个所有者都能修改它。
示例
use std::rc::Rc;
use std::cell::RefCell;
let shared_data = Rc::new(RefCell::new(10));
let owner_1 = Rc::clone(&shared_data);
let owner_2 = Rc::clone(&shared_data);
// 第一个所有者修改数据
*owner_1.borrow_mut() += 5;
// 第二个所有者也能看到修改
println!("{}", owner_2.borrow()); // 输出: 15
第二站:多线程与高并发世界 (std::sync)
进入多线程世界,我们需要能被安全地在线程间发送(Send)和共享(Sync)的类型。Rc 和 RefCell 都不符合要求。于是,它们的“线程安全”版本登场了。
1. Arc<T> (Atomically Reference Counted) - Rc 的线程安全版
- 解决什么问题? 在多线程环境下实现共享所有权。
- 工作原理:和
Rc几乎一样,但它的引用计数器是原子的。原子操作是不可分割的,可以保证在多核 CPU 上同时读写计数器也不会出错。 - 核心特点:
- 线程安全的共享所有权。
- 性能比
Rc略低,因为原子操作比普通整数操作更耗时。
2. Mutex<T> (Mutual Exclusion) - RefCell 的线程安全版
- 解决什么问题? 在多线程环境下提供内部可变性,保证同一时间只有一个线程能访问数据。
- 工作原理:通过“锁”(Lock)机制。
lock():任何线程想访问数据前,必须先调用lock()。如果锁没被占用,它获得锁并继续执行。如果锁已被其他线程占用,当前线程会阻塞(等待),直到锁被释放。Mutex返回一个MutexGuard,这是一个智能指针,它提供了对内部数据的&mut T访问。当MutexGuard离开作用域时,它会自动释放锁。这个 RAII 模式极大地保证了安全,你几乎不会忘记解锁。
- 核心特点:
- 线程安全的内部可变性。
- 通过阻塞等待来实现互斥。
终极组合技: Arc<Mutex<T>> - “银行金库”
这是 Rust 并发编程中最常见、最重要的模式。它允许你将一个可变的状态安全地在多个线程间共享和修改。
Arc:让多个线程都能“拥有”一个指向金库(Mutex)的指针(锁)。Mutex:保证同一时间只有一个线程能拿到金库的钥匙,进去操作里面的财宝(数据T)。
示例
use std::sync::{Arc, Mutex};
use std::thread;
// 创建一个被 Arc 和 Mutex 包裹的计数器
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
// 为每个线程克隆 Arc 指针
let counter_clone = Arc::clone(&counter);
let handle = thread::spawn(move || {
// 获取锁,如果被占用则等待
let mut num = counter_clone.lock().unwrap();
// 修改数据
*num += 1;
// MutexGuard 在这里离开作用域,锁被自动释放
});
handles.push(handle);
}
// 等待所有线程执行完毕
for handle in handles {
handle.join().unwrap();
}
// 打印最终结果
println!("Result: {}", *counter.lock().unwrap()); // 输出: Result: 10
总结与速查表
| 类型 | 用途 | 场景 | 线程安全? |
|---|---|---|---|
Rc<T> | 共享所有权 | 单线程 | ❌ |
RefCell<T> | 内部可变性 | 单线程 | ❌ |
Rc<RefCell<T>> | 共享所有权 + 内部可变性 | 单线程 | ❌ |
Arc<T> | 共享所有权 | 多线程 | ✅ |
Mutex<T> | 内部可变性 | 多线程 | ✅ |
Arc<Mutex<T>> | 共享所有权 + 内部可变性 | 多线程 | ✅ |