深入 Rust 引用计数智能指针:Rc 与 Arc 从入门到实战
文章目录深入 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单线程下的轻量共享什么是 RcRcT全称 Reference Counted引用计数是 Rust 标准库std::rc模块提供的智能指针专门用于单线程场景下的共享所有权。它的设计目标是轻量、高效因此内部的引用计数操作是非原子性的不具备线程安全但性能开销极小。基本用法我们用一个简单示例来说明usestd::rc::Rc;fnmain(){// 创建 Rc 智能指针包裹一个字符串计数初始化为 1letrc1Rc::new(String::from(Rust 智能指针));println!(rc1 引用计数: {},Rc::strong_count(rc1));// 输出1// 克隆 rc1计数加 1仅复制指针不复制字符串letrc2Rc::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 本身不支持可变访问但在单线程场景下我们常常需要共享且修改数据。此时可以结合另一个智能指针RefCellT单线程内部可变性容器形成RcRefCellT的组合既实现共享所有权又支持可变访问。示例单线程下共享可变的插件状态usestd::rc::Rc;usestd::cell::RefCell;// 定义插件结构体使用 RcRefCellSelf 实现共享可变typePluginRefRcRefCellPlugin;structPlugin{name:String,active:bool,// 可修改的状态}implPlugin{fnnew(name:str)-PluginRef{Rc::new(RefCell::new(Self{name:name.to_string(),active:false,}))}// 激活插件修改内部状态fnactivate(mutself){self.activetrue;println!(插件「{}」已激活,self.name);}}fnmain(){letcore_pluginPlugin::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:OptionRcNode,// 持有下一个节点的 Rc 引用}fnmain(){letnode1Rc::new(Node{value:1,next:None});letnode2Rc::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}解决方法是使用WeakT弱引用打破循环。Weak 是 Rc 的辅助类型它不参与强引用计数不会维持数据的存活仅能通过upgrade()方法临时获取强引用若数据已释放则返回None。修改后的示例用 Weak 打破循环usestd::cell::RefCell;usestd::rc::{Rc,Weak};// 节点定义next 字段使用 Weak 弱引用避免循环强引用structNode{value:i32,next:OptionWeakRefCellNode,// 弱引用指向下一个节点}fnmain(){letnode1Rc::new(RefCell::new(Node{value:1,next:None,}));letnode2Rc::new(RefCell::new(Node{value:2,next:Some(Rc::downgrade(node1)),// 弱引用}));node1.borrow_mut().nextSome(Rc::downgrade(node2));// 查看强引用计数println!(node1 强引用计数: {},Rc::strong_count(node1));// 输出2node1 自身 node2 的 nextprintln!(node2 强引用计数: {},Rc::strong_count(node2));// 输出1仅 node2 自身node1 的 next 是 Weak}Arc多线程下的线程安全共享什么是 ArcArcT全称 Atomic Reference Counted原子引用计数是 Rust 标准库std::sync模块提供的智能指针专门用于多线程场景下的共享所有权。它与 Rc 的核心区别在于引用计数的操作是原子性的。原子操作是 CPU 层面的同步指令能保证多线程同时修改计数时不会出现数据竞争因此 Arc 是线程安全的但原子操作会带来轻微的性能开销这就意味着 Arc 比 Rc 慢。只要底层数据 T 实现了Send和SyncArcT就会自动实现 Send 和 Sync可以安全地在多线程间发送和共享。基本用法我们用一个多线程共享不可变数据的示例来说明usestd::sync::Arc;usestd::thread;fnmain(){// 创建 Arc 智能指针包裹一个整数计数初始化为 1letarcArc::new(100);println!(主线程计数: {},Arc::strong_count(arc));// 输出1letmuthandlesVec::new();// 启动 5 个线程每个线程克隆 Arc 并访问数据foriin0..5{letarc_cloneArc::clone(arc);// 发送 arc_clone 到子线程Arc 是线程安全的lethandlethread::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 本身也不支持可变访问即使它是线程安全的直接修改底层数据仍会导致数据竞争。在多线程场景下需要结合MutexT互斥锁或RwLockT读写锁形成ArcMutexT的组合实现多线程共享可变数据。Mutex 的核心作用是互斥访问同一时刻只有一个线程能获取锁并修改数据其他线程会阻塞等待直到锁被释放从而避免数据竞争。以下是一个多线程共享可变计数器的示例usestd::sync::{Arc,Mutex};usestd::thread;fnmain(){// 创建 ArcMutexi32letcounterArc::new(Mutex::new(0));letmuthandlesVec::new();// 启动 10 个线程每个线程给计数器加 1for_in0..10{letcounter_cloneArc::clone(counter);lethandlethread::spawn(move||{// 获取 Mutex 锁unwrap() 处理锁获取失败的情况实际开发需谨慎letmutnumcounter_clone.lock().unwrap();// 持有锁期间修改数据其他线程会阻塞*num1;// 锁会在 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 开发中大部分共享所有权场景。