文章目录
- [深入 Rust 引用计数智能指针:Rc 与 Arc 从入门到实战](#深入 Rust 引用计数智能指针:Rc 与 Arc 从入门到实战)
-
- 引用计数:共享所有权的底层逻辑
- Rc:单线程下的轻量共享
-
- [什么是 Rc](#什么是 Rc)
- 基本用法
- [Rc + RefCell:实现单线程共享可变数据](#Rc + RefCell:实现单线程共享可变数据)
- [Rc 的陷阱:循环引用与 Weak 指针](#Rc 的陷阱:循环引用与 Weak 指针)
- Arc:多线程下的线程安全共享
-
- [什么是 Arc](#什么是 Arc)
- 基本用法
- [Arc + Mutex:实现多线程共享可变数据](#Arc + Mutex:实现多线程共享可变数据)
- 实战选型建议
- 总结
深入 Rust 引用计数智能指针:Rc 与 Arc 从入门到实战
Rust 通过所有权机制从根源上避免了悬垂指针、双重释放等内存问题,但是在实际开发中,我们常常需要让多个变量共享同一个值的所有权,比如构建树形结构、多线程共享配置等,此时就会被所有权机制搞得束手束脚。
Rust 提供了两种核心的共享所有权智能指针:Rc 和 Arc,它们都通过引用计数(Reference Counting)机制实现共享所有权,今天我们就深入拆解两者的原理、用法、区别,以及实战中的避坑技巧。
引用计数:共享所有权的底层逻辑
无论是 Rc 还是 Arc,核心都是引用计数。当我们创建一个智能指针包裹的值时,会在堆上同时存储两个关键信息:实际数据和引用计数器(记录当前有多少个智能指针指向该数据)。
引用计数的流程:
- 使用
Rc::new(T)或Arc::new(T)创建智能指针时,引用计数器初始化为 1; - 调用
clone()方法(或Rc::clone(&rc)、Arc::clone(&arc))时,不会复制底层数据,仅将引用计数器加 1; - 当某个智能指针离开作用域被销毁(drop)时,引用计数器减 1;
- 当引用计数器减至 0 时,底层数据会被自动释放,彻底避免内存泄漏。
这里需要注意:Rc/Arc 的 clone() 是浅拷贝,仅复制指针并增加计数,而非复制整个数据,因此开销极低,这也是它们能高效实现共享所有权的关键。
Rc:单线程下的轻量共享
什么是 Rc
Rc<T> 全称 Reference Counted(引用计数),是 Rust 标准库 std::rc 模块提供的智能指针,专门用于单线程场景下的共享所有权。它的设计目标是轻量、高效,因此内部的引用计数操作是非原子性的,不具备线程安全,但性能开销极小。
基本用法
我们用一个简单示例来说明:
rust
use std::rc::Rc;
fn main() {
// 创建 Rc 智能指针,包裹一个字符串,计数初始化为 1
let rc1 = Rc::new(String::from("Rust 智能指针"));
println!("rc1 引用计数: {}", Rc::strong_count(&rc1)); // 输出:1
// 克隆 rc1,计数加 1(仅复制指针,不复制字符串)
let rc2 = Rc::clone(&rc1);
println!("克隆后引用计数: {}", Rc::strong_count(&rc1)); // 输出:2
// 访问底层数据(Rc 实现了 Deref 特征,可自动解引用)
println!("rc1 内容: {}", rc1); // 输出:Rust 智能指针
println!("rc2 内容: {}", rc2); // 输出:Rust 智能指针
// 模拟 rc2 离开作用域,计数减 1
drop(rc2);
println!("rc2 销毁后计数: {}", Rc::strong_count(&rc1)); // 输出:1
} // rc1、rc2 离开作用域,计数减至 0,底层字符串被释放
Rc + RefCell:实现单线程共享可变数据
Rc 本身不支持可变访问,但在单线程场景下,我们常常需要共享且修改数据。此时可以结合另一个智能指针 RefCell<T>(单线程内部可变性容器),形成 Rc<RefCell<T>> 的组合,既实现共享所有权,又支持可变访问。
示例:单线程下共享可变的插件状态
rust
use std::rc::Rc;
use std::cell::RefCell;
// 定义插件结构体,使用 Rc<RefCell<Self>> 实现共享可变
type PluginRef = Rc<RefCell<Plugin>>;
struct Plugin {
name: String,
active: bool, // 可修改的状态
}
impl Plugin {
fn new(name: &str) -> PluginRef {
Rc::new(RefCell::new(Self {
name: name.to_string(),
active: false,
}))
}
// 激活插件(修改内部状态)
fn activate(&mut self) {
self.active = true;
println!("插件「{}」已激活", self.name);
}
}
fn main() {
let core_plugin = Plugin::new("核心模块");
// 激活核心模块(通过 RefCell 的 borrow_mut() 获取可变引用)
core_plugin.borrow_mut().activate();
println!("核心模块是否激活: {}", core_plugin.borrow().active); // 输出:true
}
Rc 的陷阱:循环引用与 Weak 指针
Rc 的引用计数机制存在一个致命问题:循环引用。如果两个对象互相持有 Rc 引用,它们的强引用计数永远不会减至 0,导致底层数据无法释放,造成内存泄漏。如下所示:
rust
use std::rc::Rc;
struct Node {
value: i32,
next: Option<Rc<Node>>, // 持有下一个节点的 Rc 引用
}
fn main() {
let node1 = Rc::new(Node { value: 1, next: None });
let node2 = Rc::new(Node { value: 2, next: Some(node1.clone()) });
// 循环引用:node1 持有 node2 的引用,node2 持有 node1 的引用
// node1.next = Some(node2.clone()); // 编译报错
// 此时 node1 和 node2 的强引用计数都是 2
println!("node1 计数: {}", Rc::strong_count(&node1)); // 输出:2
println!("node2 计数: {}", Rc::strong_count(&node2)); // 输出:2
}
解决方法是使用 Weak<T>(弱引用)打破循环。Weak 是 Rc 的辅助类型,它不参与强引用计数,不会维持数据的存活,仅能通过 upgrade() 方法临时获取强引用(若数据已释放则返回 None)。
修改后的示例(用 Weak 打破循环):
rust
use std::cell::RefCell;
use std::rc::{Rc, Weak};
// 节点定义:next 字段使用 Weak 弱引用,避免循环强引用
struct Node {
value: i32,
next: Option<Weak<RefCell<Node>>>, // 弱引用指向下一个节点
}
fn main() {
let node1 = Rc::new(RefCell::new(Node {
value: 1,
next: None,
}));
let node2 = Rc::new(RefCell::new(Node {
value: 2,
next: Some(Rc::downgrade(&node1)), // 弱引用
}));
node1.borrow_mut().next = Some(Rc::downgrade(&node2));
// 查看强引用计数
println!("node1 强引用计数: {}", Rc::strong_count(&node1)); // 输出:2(node1 自身 + node2 的 next)
println!("node2 强引用计数: {}", Rc::strong_count(&node2)); // 输出:1(仅 node2 自身,node1 的 next 是 Weak)
}
Arc:多线程下的线程安全共享
什么是 Arc
Arc<T> 全称 Atomic Reference Counted(原子引用计数),是 Rust 标准库 std::sync 模块提供的智能指针,专门用于多线程场景下的共享所有权。
它与 Rc 的核心区别在于,引用计数的操作是原子性的。原子操作是 CPU 层面的同步指令,能保证多线程同时修改计数时不会出现数据竞争,因此 Arc 是线程安全的,但原子操作会带来轻微的性能开销,这就意味着 Arc 比 Rc 慢。
只要底层数据 T 实现了 Send 和 Sync,Arc<T> 就会自动实现 Send 和 Sync,可以安全地在多线程间发送和共享。
基本用法
我们用一个多线程共享不可变数据的示例来说明:
rust
use std::sync::Arc;
use std::thread;
fn main() {
// 创建 Arc 智能指针,包裹一个整数,计数初始化为 1
let arc = Arc::new(100);
println!("主线程计数: {}", Arc::strong_count(&arc)); // 输出:1
let mut handles = Vec::new();
// 启动 5 个线程,每个线程克隆 Arc 并访问数据
for i in 0..5 {
let arc_clone = Arc::clone(&arc);
// 发送 arc_clone 到子线程(Arc 是线程安全的)
let handle = thread::spawn(move || {
println!(
"线程 {}: 数据 = {}, 计数 = {}",
i,
arc_clone,
Arc::strong_count(&arc_clone)
);
});
handles.push(handle);
}
// 等待所有子线程完成
for handle in handles {
handle.join().unwrap();
}
// 所有子线程结束,计数回到 1
println!("主线程最终计数: {}", Arc::strong_count(&arc)); // 输出:1
}
Arc + Mutex:实现多线程共享可变数据
与 Rc 类似,Arc 本身也不支持可变访问,即使它是线程安全的,直接修改底层数据仍会导致数据竞争。在多线程场景下,需要结合 Mutex<T>(互斥锁)或 RwLock<T>(读写锁),形成 Arc<Mutex<T>> 的组合,实现多线程共享可变数据。
Mutex 的核心作用是互斥访问:同一时刻只有一个线程能获取锁并修改数据,其他线程会阻塞等待,直到锁被释放,从而避免数据竞争。以下是一个多线程共享可变计数器的示例:
rust
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
// 创建 Arc<Mutex<i32>>
let counter = Arc::new(Mutex::new(0));
let mut handles = Vec::new();
// 启动 10 个线程,每个线程给计数器加 1
for _ in 0..10 {
let counter_clone = Arc::clone(&counter);
let handle = thread::spawn(move || {
// 获取 Mutex 锁(unwrap() 处理锁获取失败的情况,实际开发需谨慎)
let mut num = counter_clone.lock().unwrap();
// 持有锁期间,修改数据(其他线程会阻塞)
*num += 1;
// 锁会在 num 离开作用域时自动释放
});
handles.push(handle);
}
// 等待所有子线程完成
for handle in handles {
handle.join().unwrap();
}
// 读取最终计数(需再次获取锁)
println!("最终计数: {}", *counter.lock().unwrap()); // 输出:10
}
实战选型建议
在实际开发中,选择 Rc 还是 Arc,关键看是否需要多线程共享:
- 如果是单线程场景:优先使用 Rc,性能更优;若需要共享可变数据,搭配 RefCell;若有循环引用,用 Weak 处理。
- 如果是多线程场景:必须使用 Arc;若需要共享可变数据,搭配 Mutex(写频繁)或 RwLock(读频繁);若有循环引用,用 Weak 处理。
总结
理解两者的区别,关键在于原子操作和线程安全的权衡,Rust 没有垃圾回收(GC),却通过这种精细化的智能指针设计,既保证了内存安全,又兼顾了性能和灵活性。掌握 Rc/Arc 与 RefCell/Mutex 的组合用法,能轻松应对 Rust 开发中大部分共享所有权场景。