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

文章目录

深入 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 模块提供的智能指针,专门用于单线程场景下的共享所有权。它的设计目标是轻量、高效,因此内部的引用计数操作是非原子性的,不具备线程安全,但性能开销极小。

基本用法

我们用一个简单示例来说明:

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 实现了 SendSyncArc<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,关键看是否需要多线程共享

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

总结

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

相关推荐
CRMEB系统商城2 小时前
国内开源电商系统的格局与演变——一个务实的技术视角
java·大数据·开发语言·小程序·开源·php
树獭叔叔2 小时前
OpenCLI:让任何网站成为你的命令行工具
后端·aigc·openai
xyq20242 小时前
Eclipse 安装(Neon 版本)指南
开发语言
峥嵘life2 小时前
Android + Kiro AI软件开发实战教程
android·后端·学习
冰暮流星2 小时前
javascript之DOM更新操作
开发语言·javascript·ecmascript
飞Link2 小时前
掌控 Agent 的时空法则:LangGraph Checkpoint (检查点) 机制深度实战
开发语言·python·算法
wuyoula2 小时前
全新轻量级高性能跨平台 AI聊天+AI网关桌面
服务器·开发语言·c++·人工智能
m0_716765232 小时前
数据结构--单链表的插入、删除、查找详解
c语言·开发语言·数据结构·c++·笔记·学习·visual studio
疯狂打码的少年3 小时前
【Day13 Java转Python】装饰器、生成器与lambda——Python的函数式“三件套”
java·开发语言·python