文章目录
- 深入 Rust 引用计数智能指针:Rc 与 Arc 从入门到实战
- 引用计数:共享所有权的底层逻辑
- Rc:单线程下的轻量共享
- 什么是 Rc
- 基本用法
- Rc + RefCell:实现单线程共享可变数据
- Rc 的陷阱:循环引用与 Weak 指针
- Arc:多线程下的线程安全共享
- 什么是 Arc
- 基本用法
- 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模块提供的智能指针,专门用于单线程场景下的共享所有权。它的设计目标是轻量、高效,因此内部的引用计数操作是非原子性的,不具备线程安全,但性能开销极小。
基本用法
我们用一个简单示例来说明:
usestd::rc::Rc;fnmain(){// 创建 Rc 智能指针,包裹一个字符串,计数初始化为 1letrc1=Rc::new(String::from("Rust 智能指针"));println!("rc1 引用计数: {}",Rc::strong_count(&rc1));// 输出:1// 克隆 rc1,计数加 1(仅复制指针,不复制字符串)letrc2=Rc::clone(&rc1);println!("克隆后引用计数: {}",Rc::strong_count(&rc1));// 输出:2// 访问底层数据(Rc 实现了 Deref 特征,可自动解引用)println!("rc1 内容: {}",rc1);// 输出:Rust 智能指针println!("rc2 内容: {}",rc2);// 输出:Rust 智能指针// 模拟 rc2 离开作用域,计数减 1drop(rc2);println!("rc2 销毁后计数: {}",Rc::strong_count(&rc1));// 输出:1}// rc1、rc2 离开作用域,计数减至 0,底层字符串被释放Rc + RefCell:实现单线程共享可变数据
Rc 本身不支持可变访问,但在单线程场景下,我们常常需要共享且修改数据。此时可以结合另一个智能指针RefCell<T>(单线程内部可变性容器),形成Rc<RefCell<T>>的组合,既实现共享所有权,又支持可变访问。
示例:单线程下共享可变的插件状态
usestd::rc::Rc;usestd::cell::RefCell;// 定义插件结构体,使用 Rc<RefCell<Self>> 实现共享可变typePluginRef=Rc<RefCell<Plugin>>;structPlugin{name:String,active:bool,// 可修改的状态}implPlugin{fnnew(name:&str)->PluginRef{Rc::new(RefCell::new(Self{name:name.to_string(),active:false,}))}// 激活插件(修改内部状态)fnactivate(&mutself){self.active=true;println!("插件「{}」已激活",self.name);}}fnmain(){letcore_plugin=Plugin::new("核心模块");// 激活核心模块(通过 RefCell 的 borrow_mut() 获取可变引用)core_plugin.borrow_mut().activate();println!("核心模块是否激活: {}",core_plugin.borrow().active);// 输出:true}Rc 的陷阱:循环引用与 Weak 指针
Rc 的引用计数机制存在一个致命问题:循环引用。如果两个对象互相持有 Rc 引用,它们的强引用计数永远不会减至 0,导致底层数据无法释放,造成内存泄漏。如下所示:
usestd::rc::Rc;structNode{value:i32,next:Option<Rc<Node>>,// 持有下一个节点的 Rc 引用}fnmain(){letnode1=Rc::new(Node{value:1,next:None});letnode2=Rc::new(Node{value:2,next:Some(node1.clone())});// 循环引用:node1 持有 node2 的引用,node2 持有 node1 的引用// node1.next = Some(node2.clone()); // 编译报错// 此时 node1 和 node2 的强引用计数都是 2println!("node1 计数: {}",Rc::strong_count(&node1));// 输出:2println!("node2 计数: {}",Rc::strong_count(&node2));// 输出:2}解决方法是使用Weak<T>(弱引用)打破循环。Weak 是 Rc 的辅助类型,它不参与强引用计数,不会维持数据的存活,仅能通过upgrade()方法临时获取强引用(若数据已释放则返回None)。
修改后的示例(用 Weak 打破循环):
usestd::cell::RefCell;usestd::rc::{Rc,Weak};// 节点定义:next 字段使用 Weak 弱引用,避免循环强引用structNode{value:i32,next:Option<Weak<RefCell<Node>>>,// 弱引用指向下一个节点}fnmain(){letnode1=Rc::new(RefCell::new(Node{value:1,next:None,}));letnode2=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,可以安全地在多线程间发送和共享。
基本用法
我们用一个多线程共享不可变数据的示例来说明:
usestd::sync::Arc;usestd::thread;fnmain(){// 创建 Arc 智能指针,包裹一个整数,计数初始化为 1letarc=Arc::new(100);println!("主线程计数: {}",Arc::strong_count(&arc));// 输出:1letmuthandles=Vec::new();// 启动 5 个线程,每个线程克隆 Arc 并访问数据foriin0..5{letarc_clone=Arc::clone(&arc);// 发送 arc_clone 到子线程(Arc 是线程安全的)lethandle=thread::spawn(move||{println!("线程 {}: 数据 = {}, 计数 = {}",i,arc_clone,Arc::strong_count(&arc_clone));});handles.push(handle);}// 等待所有子线程完成forhandleinhandles{handle.join().unwrap();}// 所有子线程结束,计数回到 1println!("主线程最终计数: {}",Arc::strong_count(&arc));// 输出:1}Arc + Mutex:实现多线程共享可变数据
与 Rc 类似,Arc 本身也不支持可变访问,即使它是线程安全的,直接修改底层数据仍会导致数据竞争。在多线程场景下,需要结合Mutex<T>(互斥锁)或RwLock<T>(读写锁),形成Arc<Mutex<T>>的组合,实现多线程共享可变数据。
Mutex 的核心作用是互斥访问:同一时刻只有一个线程能获取锁并修改数据,其他线程会阻塞等待,直到锁被释放,从而避免数据竞争。以下是一个多线程共享可变计数器的示例:
usestd::sync::{Arc,Mutex};usestd::thread;fnmain(){// 创建 Arc<Mutex<i32>>letcounter=Arc::new(Mutex::new(0));letmuthandles=Vec::new();// 启动 10 个线程,每个线程给计数器加 1for_in0..10{letcounter_clone=Arc::clone(&counter);lethandle=thread::spawn(move||{// 获取 Mutex 锁(unwrap() 处理锁获取失败的情况,实际开发需谨慎)letmutnum=counter_clone.lock().unwrap();// 持有锁期间,修改数据(其他线程会阻塞)*num+=1;// 锁会在 num 离开作用域时自动释放});handles.push(handle);}// 等待所有子线程完成forhandleinhandles{handle.join().unwrap();}// 读取最终计数(需再次获取锁)println!("最终计数: {}",*counter.lock().unwrap());// 输出:10}实战选型建议
在实际开发中,选择 Rc 还是 Arc,关键看是否需要多线程共享:
- 如果是单线程场景:优先使用 Rc,性能更优;若需要共享可变数据,搭配 RefCell;若有循环引用,用 Weak 处理。
- 如果是多线程场景:必须使用 Arc;若需要共享可变数据,搭配 Mutex(写频繁)或 RwLock(读频繁);若有循环引用,用 Weak 处理。
总结
理解两者的区别,关键在于原子操作和线程安全的权衡,Rust 没有垃圾回收(GC),却通过这种精细化的智能指针设计,既保证了内存安全,又兼顾了性能和灵活性。掌握 Rc/Arc 与 RefCell/Mutex 的组合用法,能轻松应对 Rust 开发中大部分共享所有权场景。