bigywater.tech
Rust · 29

Rc<T> 与单线程共享所有权

Rc 解决的是「单线程里一份数据有多个所有者」。这篇用真实输出把它的核心行为逐条验证:计数怎么变、数据为什么只读以及怎么修改、为什么不能跨线程,以及它在内存里的样子和 clone 的成本。

24 字节
Rc::new(0u64) 在堆上分配的大小(两个计数 + 数据)
E0277
Rc 不是 Send,不能 move 进 thread::spawn
≈ 5,030 倍
Vec(1 MiB) 深克隆相对 Rc::clone 的耗时

引用计数

实测 · 本机执行输出
  源码:
      1 | use std::rc::Rc;
      2 | fn main() {
      3 |     let a = Rc::new(String::from("共享数据"));
      4 |     println!("创建 a 之后:strong_count = {}", Rc::strong_count(&a));
      5 |     let b = Rc::clone(&a);
      6 |     println!("clone 出 b 之后:strong_count = {}", Rc::strong_count(&a));
      7 |     {
      8 |         let c = Rc::clone(&a);
      9 |         println!("内层再 clone 出 c:strong_count = {}(a、b、c 指向同一块内存:{})", Rc::strong_count(&a), Rc::ptr_eq(&a, &c));
     10 |     }
     11 |     println!("c 离开作用域之后:strong_count = {}", Rc::strong_count(&a));
     12 |     drop(b);
     13 |     println!("drop(b) 之后:strong_count = {}", Rc::strong_count(&a));
     14 | }
  编译:成功
  运行(退出码 0):
    创建 a 之后:strong_count = 1
    clone 出 b 之后:strong_count = 2
    内层再 clone 出 c:strong_count = 3(a、b、c 指向同一块内存:true)
    c 离开作用域之后:strong_count = 2
    drop(b) 之后:strong_count = 1

只读与修改

实测 · 本机执行输出
  源码:
      1 | use std::rc::Rc;
      2 | fn main() {
      3 |     let a = Rc::new(vec![1, 2, 3]);
      4 |     a.push(4);
      5 | }
  编译:失败
    error[E0596]: cannot borrow data in an `Rc` as mutable
     --> <实验目录>/src/u29b.rs:4:5
      |
    4 |     a.push(4);
      |     ^ cannot borrow as mutable
      |
      = help: trait `DerefMut` is required to modify through a dereference, but it is not implemented for `Rc<Vec<i32>>`
    
    error: aborting due to 1 previous error
    
    For more information about this error, try `rustc --explain E0596`.
  源码:
      1 | use std::rc::Rc;
      2 | fn main() {
      3 |     let mut a = Rc::new(vec![1, 2, 3]);
      4 |     println!("独占时 get_mut 成功:{}", Rc::get_mut(&mut a).map(|v| { v.push(4); true }).unwrap_or(false));
      5 |     let b = Rc::clone(&a);
      6 |     println!("有第二个所有者时 get_mut 返回 None:{}", Rc::get_mut(&mut a).is_none());
      7 |     Rc::make_mut(&mut a).push(5);          // 写时复制:发现不是唯一所有者,先克隆一份
      8 |     println!("make_mut 之后:a = {:?},b = {:?},a 与 b 还指向同一块内存吗?{}", a, b, Rc::ptr_eq(&a, &b));
      9 | }
  编译:成功
  运行(退出码 0):
    独占时 get_mut 成功:true
    有第二个所有者时 get_mut 返回 None:true
    make_mut 之后:a = [1, 2, 3, 4, 5],b = [1, 2, 3, 4],a 与 b 还指向同一块内存吗?false

不是 Send

实测 · 本机执行输出
  源码:
      1 | use std::rc::Rc;
      2 | use std::thread;
      3 | fn main() {
      4 |     let a = Rc::new(5);
      5 |     let h = thread::spawn(move || println!("{}", a));
      6 |     h.join().unwrap();
      7 | }
  编译:失败
    error[E0277]: `Rc<i32>` cannot be sent between threads safely
       --> <实验目录>/src/u29d.rs:5:27
        |
      5 |     let h = thread::spawn(move || println!("{}", a));
        |             ------------- -------^^^^^^^^^^^^^^^^^^
        |             |             |
        |             |             `Rc<i32>` cannot be sent between threads safely
        |             |             within this `{closure@u29d.rs:5:27}`
        |             required by a bound introduced by this call
        |
        = help: within `{closure@u29d.rs:5:27}`, the trait `Send` is not implemented for `Rc<i32>`
    note: required because it's used within this closure
       --> <实验目录>/src/u29d.rs:5:27
        |
      5 |     let h = thread::spawn(move || println!("{}", a));
        |                           ^^^^^^^
    note: required by a bound in `spawn`
       --> <rust 标准库源码>/library/std/src/thread/functions.rs:128:8
        |
    125 | pub fn spawn<F, T>(f: F) -> JoinHandle<T>
        |        ----- required by a bound in this function
    ...
    128 |     F: Send + 'static,
        |        ^^^^ required by this bound in `spawn`
        = note: the full name for the type has been written to '<实验目录>/bin/u29d.long-type-13928888389995968006.txt'
        = note: consider using `--verbose` to print the full type name to the console
    
    error: aborting due to 1 previous error
    
    For more information about this error, try `rustc --explain E0277`.

多线程要用 Arc,见Arc<T> 与跨线程共享所有权;Send 的含义见Send 与 Sync。

布局与成本

实测 · 本机执行输出
  编译:成功
  运行(退出码 0):
    size_of::<Rc<u64>>() = 8 字节(指针);Rc::new(0u64) 在堆上分配了 24 字节(强引用计数 8 + 弱引用计数 8 + 数据 8)
    Rc::clone(共享 1 MiB 数据):每次约 3.9 ns
    Vec<u8>(1 MiB) 的 clone:每次约 19616 ns

循环引用会让计数永远不归零,见Weak<T> 解决循环引用;需要修改共享数据时配合RefCell。内存布局见Box、Rc、Arc 的内存布局。

分步演示

Rc<T>
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / rustc 1.98.1(brew)/ cargo 1.98.1;单个文件用 rustc 直接编译,需要依赖时用 cargo(经代理从 crates.io 下载);计时用 std::time::Instant,数字每次运行会有小幅抖动。计时为 release 构建的单次运行
    • 没有验证:Rc 的弱引用计数在 Weak 存在时的变化(见 Weak 一篇)、Rc 与 Rc<[T]>、Rc::try_unwrap、Rc 的 clone 在优化后是否被消除(只测了单次 clone 的循环平均)
    京ICP备2026067502号-1京公网安备11010502063487号