学习笔记 20240807 Rust语言-Rc 与 Arc,Cell 与 RefCell 内部可变性

20240807

Rc 与 Arc

Rust 所有权机制要求一个值只能有一个所有者,在大多数情况下,都没有问题,但是考虑以下情况:

  • 在图数据结构中,多个边可能会拥有同一个节点,该节点直到没有边指向它时,才应该被释放清理
  • 在多线程中,多个线程可能会持有同一个数据,但是你受限于 Rust 的安全机制,无法同时获取该数据的可变引用

以上场景不是很常见,但是一旦遇到,就非常棘手,为了解决此类问题,Rust 在所有权机制之外又引入了额外的措施来简化相应的实现:通过引用计数的方式,允许一个数据资源在同一时刻拥有多个所有者。

这种实现机制就是 Rc 和 Arc,前者适用于单线程,后者适用于多线程。由于二者大部分情况下都相同,因此本章将以 Rc 作为讲解主体,对于 Arc 的不同之处,另外进行单独讲解。

Rc<T>

引用计数(reference counting),顾名思义,通过记录一个数据被引用的次数来确定该数据是否正在被使用。当引用次数归零时,就代表该数据不再被使用,因此可以被清理释放。

而 Rc 正是引用计数的英文缩写。当我们希望在堆上分配一个对象供程序的多个部分使用且无法确定哪个部分最后一个结束时,就可以使用 Rc 成为数据值的所有者,例如之前提到的多线程场景就非常适合。

下面是经典的所有权被转移导致报错的例子:

fn main() {
    let s = String::from("hello, world");
    // s在这里被转移给a
    let a = Box::new(s);
    // 报错!此处继续尝试将 s 转移给 b
    let b = Box::new(s);
}

使用Rc:

use std::rc::Rc;
fn main() {
    let a = Rc::new(String::from("hello, world"));
    assert_eq!(1, Rc::strong_count(&a));
    let b = Rc::clone(&a);

    assert_eq!(2, Rc::strong_count(&a));
    assert_eq!(Rc::strong_count(&a), Rc::strong_count(&b))
}

其中Rc::clone( ) 的解释

alloc::rc::Rc
impl<T, A> Clone for Rc<T, A>
fn clone(&self) -> Self
where
    // Bounds from impl:
    T: ?Sized,
    A: Allocator + Clone,

Makes a clone of the Rc pointer.

This creates another pointer to the same allocation, increasing the strong reference count

Rc::strong_count( )的解释

alloc::rc::Rc
impl<T, A> Rc<T, A>
pub fn strong_count(this: &Self) -> usize
where
    // Bounds from impl:
    T: ?Sized,
    A: Allocator,

Gets the number of strong (Rc) pointers to this allocation.

以上代码我们使用 Rc::new 创建了一个新的 Rc<String> 智能指针并赋给变量 a,该指针指向底层的字符串数据。

智能指针 Rc<T> 在创建时,还会将引用计数加 1,此时获取引用计数的关联函数 Rc::strong_count 返回的值将是 1。

接着,我们又使用 Rc::clone 克隆了一份智能指针 Rc<String>,并将该智能指针的引用计数增加到 2。

由于 a 和 b 是同一个智能指针的两个副本,因此通过它们两个获取引用计数的结果都是 2。

不要被 clone 字样所迷惑,以为所有的 clone 都是深拷贝。这里的 clone 仅仅复制了智能指针并增加了引用计数,并没有克隆底层数据,因此 a 和 b 是共享了底层的字符串 s,这种复制效率是非常高的。当然你也可以使用 a.clone() 的方式来克隆,但是从可读性角度,我们更加推荐 Rc::clone 的方式。

实际上在 Rust 中,还有不少 clone 都是浅拷贝,例如迭代器的克隆

观察引用计数的变化

使用关联函数 Rc::strong_count 可以获取当前引用计数的值,我们来观察下引用计数如何随着变量声明、释放而变化:

use std::rc::Rc;
fn main() {
        let a = Rc::new(String::from("test ref counting"));
        println!("count after creating a = {}", Rc::strong_count(&a));
        let b =  Rc::clone(&a);
        println!("count after creating b = {}", Rc::strong_count(&a));
        {
            let c =  Rc::clone(&a);
            println!("count after creating c = {}", Rc::strong_count(&c));
        }
        println!("count after c goes out of scope = {}", Rc::strong_count(&a));
}

输出如下:

count after creating a = 1
count after creating b = 2
count after creating c = 3
count after c goes out of scope = 2

有几点值得注意:

  • 由于变量 c 在语句块内部声明,当离开语句块时它会因为超出作用域而被释放,所以引用计数会减少 1,事实上这个得益于 Rc<T> 实现了 Drop 特征
  • a、b、c 三个智能指针引用计数都是同样的,并且共享底层的数据,因此打印计数时用哪个都行
  • 无法看到的是:当 a、b 超出作用域后,引用计数会变成 0,最终智能指针和它指向的底层字符串都会被清理释放
不可变引用

事实上,Rc<T> 是指向底层数据的不可变的引用,因此你无法通过它来修改数据,这也符合 Rust 的借用规则:要么存在多个不可变借用,要么只能存在一个可变借用。

但是实际开发中我们往往需要对数据进行修改,这时单独使用 Rc<T> 无法满足我们的需求,需要配合其它数据类型来一起使用,例如内部可变性的 RefCell<T> 类型以及互斥锁 Mutex<T>。事实上,在多线程编程中,Arc 跟 Mutex 锁的组合使用非常常见,它们既可以让我们在不同的线程中共享数据,又允许在各个线程中对其进行修改。

一个综合例子

考虑一个场景,有很多小工具,每个工具都有自己的主人,但是存在多个工具属于同一个主人的情况,此时使用 Rc<T> 就非常适合:

use std::rc::Rc;

struct Owner {
    name: String,
    // ...其它字段
}

struct Gadget {
    id: i32,
    owner: Rc<Owner>,
    // ...其它字段
}

fn main() {
    // 创建一个基于引用计数的 `Owner`.
    let gadget_owner: Rc<Owner> = Rc::new(Owner {
        name: "Gadget Man".to_string(),
    });

    // 创建两个不同的工具,它们属于同一个主人
    let gadget1 = Gadget {
        id: 1,
        owner: Rc::clone(&gadget_owner),
    };
    let gadget2 = Gadget {
        id: 2,
        owner: Rc::clone(&gadget_owner),
    };

    // 释放掉第一个 `Rc<Owner>`
    drop(gadget_owner);

    // 尽管在上面我们释放了 gadget_owner,但是依然可以在这里使用 owner 的信息
    // 原因是在 drop 之前,存在三个指向 Gadget Man 的智能指针引用,上面仅仅
    // drop 掉其中一个智能指针引用,而不是 drop 掉 owner 数据,外面还有两个
    // 引用指向底层的 owner 数据,引用计数尚未清零
    // 因此 owner 数据依然可以被使用
    println!("Gadget {} owned by {}", gadget1.id, gadget1.owner.name);
    println!("Gadget {} owned by {}", gadget2.id, gadget2.owner.name);

    // 在函数最后,`gadget1` 和 `gadget2` 也被释放,最终引用计数归零,随后底层
    // 数据也被清理释放
}

输出如下:

Gadget 1 owned by Gadget Man
Gadget 2 owned by Gadget Man

以上代码很好的展示了 Rc<T> 的用途,当然你也可以用借用的方式,但是实现起来就会复杂得多,而且随着 Gadget 在代码的各个地方使用,引用生命周期也将变得更加复杂.

Rc 简单总结
  • Rc/Arc 是不可变引用,你无法修改它指向的值,只能进行读取,如果要修改,需要配合后面章节的内部可变性 RefCell 或互斥锁 Mutex
  • 一旦最后一个拥有者消失,则资源会自动被回收,这个生命周期是在编译期就确定下来的
  • Rc 只能用于同一线程内部,想要用于线程之间的对象共享,你需要使用 Arc
  • Rc<T> 是一个智能指针,实现了 Deref 特征,因此你无需先解开 Rc 指针,再使用里面的 T,而是可以直接使用 T,例如上例中的"…gadget1.owner.name"
多线程无力的 Rc

来看看在多线程场景使用 Rc<T> 会如何:

use std::rc::Rc;
use std::thread;

fn main() {
    let s = Rc::new(String::from("多线程漫游者"));
    for _ in 0..10 {
        let s = Rc::clone(&s);
        let handle = thread::spawn(move || {
           println!("{}", s)
        });
    }
}

由于我们还没有学习多线程的章节,上面的例子就特地简化了相关的实现。首先通过 thread::spawn 创建一个线程,然后使用 move 关键字把克隆出的 s 的所有权转移到线程中。

能够实现这一点,完全得益于 Rc 带来的多所有权机制,但是以上代码会报错:

error[E0277]: `Rc<String>` cannot be sent between threads safely

表面原因是 Rc<T> 不能在线程间安全的传递,实际上是因为它没有实现 Send 特征,而该特征是恰恰是多线程间传递数据的关键,我们会在多线程章节中进行讲解。

当然,还有更深层的原因:由于 Rc<T> 需要管理引用计数,但是该计数器并没有使用任何并发原语,因此无法实现原子化的计数操作,最终会导致计数错误。

好在天无绝人之路,一起来看看 Rust 为我们提供的功能类似但是多线程安全的 Arc。

Arc

Arc 是 Atomic Rc 的缩写,顾名思义:原子化的 Rc<T> 智能指针。原子化是一种并发原语,我们在后续章节会进行深入讲解,这里你只要知道它能保证我们的数据能够安全的在线程间共享即可。

Arc 的性能损耗

你可能好奇,为何不直接使用 Arc,还要画蛇添足弄一个 Rc,还有 Rust 的基本数据类型、标准库数据类型为什么不自动实现原子化操作?这样就不存在线程不安全的问题了。

原因在于原子化或者其它锁虽然可以带来的线程安全,但是都会伴随着性能损耗,而且这种性能损耗还不小。因此 Rust 把这种选择权交给你,毕竟需要线程安全的代码其实占比并不高,大部分时候我们开发的程序都在一个线程内。

Arc 和 Rc 拥有完全一样的 API,修改起来很简单:

use std::sync::Arc;
use std::thread;

fn main() {
    let s = Arc::new(String::from("多线程漫游者"));
    for _ in 0..10 {
        let s = Arc::clone(&s);
        let handle = thread::spawn(move || {
           println!("{}", s)
        });
    }
}

对了,两者还有一点区别:Arc 和 Rc 并没有定义在同一个模块,前者通过 use std::sync::Arc 来引入,后者通过 use std::rc::Rc。

总结

在 Rust 中,所有权机制保证了一个数据只会有一个所有者,但如果你想要在图数据结构、多线程等场景中共享数据,这种机制会成为极大的阻碍。好在 Rust 为我们提供了智能指针 Rc 和 Arc,使用它们就能实现多个所有者共享一个数据的功能。

Rc 和 Arc 的区别在于,后者是原子化实现的引用计数,因此是线程安全的,可以用于多线程中共享数据。

这两者都是只读的,如果想要实现内部数据可修改,必须配合内部可变性 RefCell 或者互斥锁 Mutex 来一起使用。

Cell 和 RefCell

Rust 的编译器之严格,可以说是举世无双。特别是在所有权方面,Rust 通过严格的规则来保证所有权和借用的正确性,最终为程序的安全保驾护航。

但是严格是一把双刃剑,带来安全提升的同时,损失了灵活性,有时甚至会让用户痛苦不堪、怨声载道。因此 Rust 提供了 Cell 和 RefCell 用于内部可变性,简而言之,可以在拥有不可变引用的同时修改目标数据,对于正常的代码实现来说,这个是不可能做到的(要么一个可变借用,要么多个不可变借用)。

内部可变性的实现是因为 Rust 使用了 unsafe 来做到这一点,但是对于使用者来说,这些都是透明的,因为这些不安全代码都被封装到了安全的 API 中

Cell

Cell 和 RefCell 在功能上没有区别,区别在于 Cell<T> 适用于 T 实现 Copy 的情况:

use std::cell::Cell;
fn main() {
  let c = Cell::new("asdf");
  let one = c.get();
  c.set("qwer");
  let two = c.get();
  println!("{},{}", one, two);
}

输出如下:

asdf,qwer

以上代码展示了 Cell 的基本用法,有几点值得注意:

  • “asdf” 是 &str 类型,它实现了 Copy 特征
  • c.get 用来取值,c.set 用来设置新值

取到值保存在 one 变量后,还能同时进行修改,这个违背了 Rust 的借用规则,但是由于 Cell 的存在,我们很优雅地做到了这一点,但是如果你尝试在 Cell 中存放String:

 let c = Cell::new(String::from("asdf"));

编译器会立刻报错,因为 String 没有实现 Copy 特征:

| pub struct String {
| ----------------- doesn't satisfy `String: Copy`
|
= note: the following trait bounds were not satisfied:
        `String: Copy`

RefCell

由于 Cell 类型针对的是实现了 Copy 特征的值类型,因此在实际开发中,Cell 使用的并不多,因为我们要解决的往往是可变、不可变引用共存导致的问题,此时就需要借助于 RefCell 来达成目的。

我们可以将所有权、借用规则与这些智能指针做一个对比:

Rust 规则智能指针带来的额外规则
一个数据只有一个所有者Rc/Arc让一个数据可以拥有多个所有者
要么多个不可变借用,要么一个可变借用RefCell实现编译期可变、不可变引用共存
违背规则导致编译错误违背规则导致运行时panic

可以看出,Rc/Arc 和 RefCell 合在一起,解决了 Rust 中严苛的所有权和借用规则带来的某些场景下难使用的问题。但是它们并不是银弹,例如 RefCell 实际上并没有解决可变引用和引用可以共存的问题,只是将报错从编译期推迟到运行时,从编译器错误变成了 panic 异常:

use std::cell::RefCell;

fn main() {
    let s = RefCell::new(String::from("hello, world"));
    let s1 = s.borrow();
    let s2 = s.borrow_mut();

    println!("{},{}", s1, s2);
}

上面代码在编译期不会报任何错误,你可以顺利运行程序:

thread 'main' panicked at 'already borrowed: BorrowMutError', src/main.rs:6:16
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

但是依然会因为违背了借用规则导致了运行期 panic

这里介绍RefCell的borrow( )和borrow_mut( ):

use std::cell::RefCell;

fn main() {
    let s = RefCell::new(String::from("hello, world"));
    // s 为 RefCell<String>类型,没有Deref特征,不能用*s解引用
    let s1 = s.borrow();
    // s1 为 Ref<String>类型,表示是RefCell<String>类型的不可变引用;有Deref特征,*s1得到String类型,&s1可以得到&String类型
    let s2 = s.borrow_mut();
    // s2 为 RefMut<String> 类型,表示是RefCell<String>类型的可变引用;有Deref特征,*s2得到String类型,&s2可以得到&String类型

    println!("{},{}", s1, s2);
    // 编译期不报错,但是实际上这里是可变引用和不可变引用同时存在,运行时会panic
}
RefCell 为何存在

相信肯定有读者有疑问了,这么做有任何意义吗?还不如在编译期报错,至少能提前发现问题,而且性能还更好。

存在即合理,究其根因,在于 Rust 编译期的宁可错杀,绝不放过的原则,当编译器不能确定你的代码是否正确时,就统统会判定为错误,因此难免会导致一些误报。

而 RefCell 正是用于你确信代码是正确的,而编译器却发生了误判时。

对于大型的复杂程序,也可以选择使用 RefCell 来让事情简化。例如在 Rust 编译器的ctxt结构体中有大量的 RefCell 类型的 map 字段,主要的原因是:这些 map 会被分散在各个地方的代码片段所广泛使用或修改。由于这种分散在各处的使用方式,导致了管理可变和不可变成为一件非常复杂的任务(甚至不可能),你很容易就碰到编译器抛出来的各种错误。而且 RefCell 的运行时错误在这种情况下也变得非常可爱:一旦有人做了不正确的使用,代码会 panic,然后告诉我们哪些借用冲突了。

总之,当你确信编译器误报但不知道该如何解决时,或者你有一个引用类型,需要被四处使用和修改然后导致借用关系难以管理时,都可以优先考虑使用 RefCell。

  • 与 Cell 用于可 Copy 的值不同,RefCell 用于引用
  • RefCell 只是将借用规则从编译期推迟到程序运行期,并不能帮你绕过这个规则
  • RefCell 适用于编译期误报或者一个引用被在多处代码使用、修改以至于难于管理借用关系时
  • 使用 RefCell 时,违背借用规则会导致运行期的 panic

选择 Cell 还是 RefCell

根据本文的内容,我们可以大概总结下两者的区别:

  • Cell 只适用于 Copy 类型,用于提供值,而 RefCell 用于提供引用
  • Cell 不会 panic,而 RefCell 会
性能比较

Cell 没有额外的性能损耗,例如以下两段代码的性能其实是一致的:

// code snipet 1
let x = Cell::new(1);
let y = &x;
let z = &x;
x.set(2);
y.set(3);
z.set(4);
println!("{}", x.get());

// code snipet 2
let mut x = 1;
let y = &mut x;
let z = &mut x;
x = 2;
*y = 3;
*z = 4;
println!("{}", x);

虽然性能一致,但代码 1 拥有代码 2 不具有的优势:它能编译成功 😃

与 Cell 的 zero cost 不同,RefCell 其实是有一点运行期开销的,原因是它包含了一个字节大小的“借用状态”指示器,该指示器在每次运行时借用时都会被修改,进而产生一点开销。

总之,当非要使用内部可变性时,首选 Cell,只有你的类型没有实现 Copy 时,才去选择 RefCell。

内部 可变性

我认为内部可变性是RefCell的一个比较重要的性质和应用

之前我们提到 RefCell 具有内部可变性,何为内部可变性?简单来说,对一个不可变的值进行可变借用,但这个并不符合 Rust 的基本借用规则:

fn main() {
    let x = 5;
    let y = &mut x;
}

上面的代码会报错,因为我们不能对一个不可变的值进行可变借用,这会破坏 Rust 的安全性保证,相反,你可以对一个可变值进行不可变借用。原因是:当值不可变时,可能会有多个不可变的引用指向它,此时若将其中一个修改为可变的,会造成可变引用与不可变引用共存的情况;而当值可变时,最多只会有一个可变引用指向它,将其修改为不可变,那么最终依然是只有一个不可变的引用指向它。

虽然基本借用规则是 Rust 的基石,然而在某些场景中,一个值可以在其方法内部被修改,同时对于其它代码不可变,是很有用的:

// 定义在外部库中的特征
pub trait Messenger {
    fn send(&self, msg: String);
}

// --------------------------
// 我们的代码中的数据结构和实现
struct MsgQueue {
    msg_cache: Vec<String>,//动态数组类型,元素为String类型
}

impl Messenger for MsgQueue {
    fn send(&self, msg: String) {
        self.msg_cache.push(msg)
        // push(msg)是在msg_cache添加一个String类型数据,因此要求msg_cache是可变的
    }
}

如上所示,外部库中定义了一个消息发送器特征 Messenger,它只有一个发送消息的功能:fn send(&self, msg: String),因为发送消息不需要修改自身,因此原作者在定义时,使用了 &self 的不可变借用,这个无可厚非。

我们要在自己的代码中使用该特征实现一个异步消息队列,出于性能的考虑,消息先写到本地缓存(内存)中,然后批量发送出去,因此在 send 方法中,需要将消息先行插入到本地缓存 msg_cache 中。但是问题来了,该 send 方法的签名是 &self,因此上述代码会报错:

error[E0596]: cannot borrow `self.msg_cache` as mutable, as it is behind a `&` reference
  --> src/main.rs:11:9
   |
2  |     fn send(&self, msg: String);
   |             ----- help: consider changing that to be a mutable reference: `&mut self`
...
11 |         self.msg_cache.push(msg)
   |         ^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be borrowed as mutable

在报错的同时,编译器大聪明还善意地给出了提示:将 &self 修改为 &mut self,但是。。。我们实现的特征是定义在外部库中,因此需要在该签名不能修改的条件下去实现我们想要的效果。

综上,我们要解决的问题就是:

  • Messenger特征内部,send方法的不可变借用签名&self不能更改
  • 需要在传入一个&self的MsgQueue类型后,使该类型内部的 msg_cache 成为一个可变值,然后实现对其的修改

值此危急关头, RefCell 闪亮登场:

use std::cell::RefCell;
pub trait Messenger {
    fn send(&self, msg: String);
}

pub struct MsgQueue {
    msg_cache: RefCell<Vec<String>>,// 修改发生在此处
}

impl Messenger for MsgQueue {
    fn send(&self, msg: String) {
        self.msg_cache.borrow_mut().push(msg) //修改发生在此处
    }
}

fn main() {
    let mq = MsgQueue {
        msg_cache: RefCell::new(Vec::new()),
    };
    mq.send("hello, world".to_string());
}

这个 MQ 功能很弱,但是并不妨碍我们演示内部 可变性 的核心用法:通过包裹一层 RefCell,成功的让 &self 中的 msg_cache 成为一个可变值,然后实现对其的修改。 其中包裹修改这两个词分别体现出了 内部可变性
并且,我们并没有触发panic,意味着遵守了Rust的基本借用规则。那么对于RefCell的使用,我们要如何判断是否遵守Rust的基本借用规则呢?

  • 只需要看borrow( )和borrow_mut( )的位置,因为这两个方法表示对RefCell<T>类型的不可变引用和可变引用,分别表示Ref<T>和RefMut<T>类型
  • 只要在同一个作用域中,只出现若干个Ref<T>类型,或者只出现一个RefMut<T>类型,即满足Rust的基本借用规则

Rc + RefCell 组合使用

在 Rust 中,一个常见的组合就是 Rc 和 RefCell 在一起使用,前者可以实现一个数据拥有多个所有者,后者可以实现数据的可变性:

use std::cell::RefCell;
use std::rc::Rc;
fn main() {
    let s = Rc::new(RefCell::new("我很善变,还拥有多个主人".to_string()));

    let s1 = s.clone();
    let s2 = s.clone();
    // let mut s2 = s.borrow_mut();
    s2.borrow_mut().push_str(", oh yeah!");

    println!("{:?}\n{:?}\n{:?}", s, s1, s2);
}

上面代码中,我们使用 RefCell<String> 包裹一个字符串,同时通过 Rc 创建了它的三个所有者:s、s1和s2,并且通过其中一个所有者 s2 对字符串内容进行了修改。

由于 Rc 的所有者们共享同一个底层的数据,因此当一个所有者修改了数据时,会导致全部所有者持有的数据都发生了变化。

程序的运行结果也在预料之中:

RefCell { value: "我很善变,还拥有多个主人, oh yeah!" }
RefCell { value: "我很善变,还拥有多个主人, oh yeah!" }
RefCell { value: "我很善变,还拥有多个主人, oh yeah!" }

性能损耗

相信这两者组合在一起使用时,很多人会好奇到底性能如何,下面我们来简单分析下。

首先给出一个大概的结论,这两者结合在一起使用的性能其实非常高,大致相当于没有线程安全版本的 C++ std::shared_ptr 指针,事实上,C++ 这个指针的主要开销也在于原子性这个并发原语上,毕竟线程安全在哪个语言中开销都不小。

内存损耗

两者结合的数据结构与下面类似:

struct Wrapper<T> {
    // Rc
    strong_count: usize,
    weak_count: usize,

    // Refcell
    borrow_count: isize,

    // 包裹的数据
    item: T,
}

从上面可以看出,从对内存的影响来看,仅仅多分配了三个usize/isize,并没有其它额外的负担。

CPU 损耗

从 CPU 来看,损耗如下:

  • 对 Rc<T> 解引用是免费的(编译期),但是 * 带来的间接取值并不免费
  • 克隆 Rc<T> 需要将当前的引用计数跟 0 和 usize::Max 进行一次比较,然后将计数值加 1
  • 释放(drop) Rc<T> 需要将计数值减 1, 然后跟 0 进行一次比较
  • 对 RefCell 进行不可变借用,需要将 isize 类型的借用计数加 1,然后跟 0 进行比较
  • 对 RefCell 的不可变借用进行释放,需要将 isize 减 1
  • 对 RefCell 的可变借用大致流程跟上面差不多,但是需要先跟 0 比较,然后再减 1
  • 对 RefCell 的可变借用进行释放,需要将 isize 加 1

其实这些细节不必过于关注,只要知道 CPU 消耗也非常低,甚至编译器还会对此进行进一步优化!

CPU 缓存 Miss

唯一需要担心的可能就是这种组合数据结构对于 CPU 缓存是否亲和,这个我们无法证明,只能提出来存在这个可能性,最终的性能影响还需要在实际场景中进行测试。

总之,分析这两者组合的性能还挺复杂的,大概总结下:

  • 从表面来看,它们带来的内存和 CPU 损耗都不大
  • 但是由于 Rc 额外的引入了一次间接取值(*),在少数场景下可能会造成性能上的显著损失
  • CPU 缓存可能也不够亲和

总结

Cell 和 RefCell 都为我们带来了内部可变性这个重要特性,同时还将借用规则的检查从编译期推迟到运行期,但是这个检查并不能被绕过,该来早晚还是会来,RefCell 在运行期的报错会造成 panic。

RefCell 适用于编译器误报或者一个引用被在多个代码中使用、修改以至于难于管理借用关系时,还有就是需要内部可变性时。

从性能上看,RefCell 由于是非线程安全的,因此无需保证原子性,性能虽然有一点损耗,但是依然非常好,而 Cell 则完全不存在任何额外的性能损耗。

Rc 跟 RefCell 结合使用可以实现多个所有者共享同一份数据,非常好用,但是潜在的性能损耗也要考虑进去,建议对于热点代码使用时,做好 benchmark。

参考文献

Rust语言圣经-Rc 与 Arc 实现 1vN 所有权机制

Rust语言圣经-Cell 与 RefCell 内部可变性

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值