news 2026/4/16 13:58:31

深入 Rust 引用计数智能指针:Rc 与 Arc 从入门到实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
深入 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,核心都是引用计数。当我们创建一个智能指针包裹的值时,会在堆上同时存储两个关键信息:实际数据和引用计数器(记录当前有多少个智能指针指向该数据)。

引用计数的流程:

  1. 使用Rc::new(T)Arc::new(T)创建智能指针时,引用计数器初始化为 1;
  2. 调用clone()方法(或Rc::clone(&rc)Arc::clone(&arc))时,不会复制底层数据,仅将引用计数器加 1;
  3. 当某个智能指针离开作用域被销毁(drop)时,引用计数器减 1;
  4. 当引用计数器减至 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 实现了SendSyncArc<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,关键看是否需要多线程共享

  1. 如果是单线程场景:优先使用 Rc,性能更优;若需要共享可变数据,搭配 RefCell;若有循环引用,用 Weak 处理。
  2. 如果是多线程场景:必须使用 Arc;若需要共享可变数据,搭配 Mutex(写频繁)或 RwLock(读频繁);若有循环引用,用 Weak 处理。

总结

理解两者的区别,关键在于原子操作和线程安全的权衡,Rust 没有垃圾回收(GC),却通过这种精细化的智能指针设计,既保证了内存安全,又兼顾了性能和灵活性。掌握 Rc/Arc 与 RefCell/Mutex 的组合用法,能轻松应对 Rust 开发中大部分共享所有权场景。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/16 13:57:35

从CTF实战到漏洞挖掘:2025年广西网安赛题WriteUP深度解析

1. 从CTF签到题看信息隐藏技巧 2025年广西网安赛的Misc签到题看似简单&#xff0c;却暗藏玄机。题目给出一个加密的ZIP压缩包&#xff0c;提示需要爆破六位数密码。这里就涉及到CTF比赛中常见的信息隐藏三板斧&#xff1a; 弱密码爆破&#xff1a;使用工具如John the Ripper或f…

作者头像 李华
网站建设 2026/4/16 13:57:33

MSPM0L ADC+DMA实战:用定时器触发实现1.45Msps连续采样(附完整代码)

MSPM0L ADCDMA实战&#xff1a;1.45Msps高采样率系统设计与调优指南 在嵌入式信号采集领域&#xff0c;实现高采样率、低延迟的数据采集一直是工程师面临的挑战。MSPM0系列微控制器凭借其高性能ADC和灵活的DMA架构&#xff0c;为构建1.45Msps级别的采集系统提供了硬件基础。本文…

作者头像 李华
网站建设 2026/4/16 13:57:30

AI智能体在渗透测试中的实战技巧与自动化策略

1. AI智能体如何重塑渗透测试工作流 记得我第一次用AI智能体做渗透测试时&#xff0c;整个人都惊呆了。原本需要3天才能完成的信息收集工作&#xff0c;AI只用了20分钟就给出了更全面的报告。这种效率提升不是简单的量变&#xff0c;而是整个工作模式的质变。 传统渗透测试就…

作者头像 李华
网站建设 2026/4/16 13:55:39

DS4Windows终极指南:5步解决PS4手柄PC游戏兼容性问题

DS4Windows终极指南&#xff1a;5步解决PS4手柄PC游戏兼容性问题 【免费下载链接】DS4Windows Like those other ds4tools, but sexier 项目地址: https://gitcode.com/gh_mirrors/ds/DS4Windows 你是否曾遇到过这样的尴尬场景&#xff1a;新买的PS4手柄连接到PC后&…

作者头像 李华
网站建设 2026/4/16 13:52:26

惠普OMEN游戏本性能优化神器:OmenSuperHub深度解析与使用指南

惠普OMEN游戏本性能优化神器&#xff1a;OmenSuperHub深度解析与使用指南 【免费下载链接】OmenSuperHub 使用 WMI BIOS控制性能和风扇速度&#xff0c;自动解除DB功耗限制。 项目地址: https://gitcode.com/gh_mirrors/om/OmenSuperHub 还在为官方Omen Gaming Hub的臃肿…

作者头像 李华
网站建设 2026/4/16 13:51:42

5分钟快速上手:OBS智能背景移除插件完整配置指南

5分钟快速上手&#xff1a;OBS智能背景移除插件完整配置指南 【免费下载链接】obs-backgroundremoval An OBS plugin for removing background in portrait images (video), making it easy to replace the background when recording or streaming. 项目地址: https://gitco…

作者头像 李华