|
|
@ -1,7 +1,7 @@
|
|
|
|
# Cell 和 RefCell
|
|
|
|
# Cell 和 RefCell
|
|
|
|
Rust 的编译器之严格,可以说是举世无双。特别是在所有权方面,Rust 通过严格的规则来保证所有权和借用的正确性,最终为程序的安全保驾护航。
|
|
|
|
Rust 的编译器之严格,可以说是举世无双。特别是在所有权方面,Rust 通过严格的规则来保证所有权和借用的正确性,最终为程序的安全保驾护航。
|
|
|
|
|
|
|
|
|
|
|
|
但是严格是一把双刃剑,带来安全提升的同时,损失了灵活性,有时甚至会让用户痛苦不堪、怨声载道。因此 Rust 提供了 `Cell` 和 `RefCell` 用于内部可变性,简而言之,可以在拥有不可变引用的同时修改目标数据,对于正常的代码实现来说,这个是不可能做到的(要么一个可变借用,要么多个不可变借用)。
|
|
|
|
但是严格是一把双刃剑,带来安全提升的同时,损失了灵活性,有时甚至会让用户痛苦不堪、怨声载道。因此 Rust 提供了 `Cell` 和 `RefCell` 用于内部可变性,简而言之,可以在拥有不可变引用的同时修改目标数据,对于正常的代码实现来说,这个是不可能做到的(要么一个可变借用,要么多个不可变借用)。
|
|
|
|
|
|
|
|
|
|
|
|
> 内部可变性的实现是因为 Rust 使用了 `unsafe` 来做到这一点,但是对于使用者来说,这些都是透明的,因为这些不安全代码都被封装到了安全的 API 中
|
|
|
|
> 内部可变性的实现是因为 Rust 使用了 `unsafe` 来做到这一点,但是对于使用者来说,这些都是透明的,因为这些不安全代码都被封装到了安全的 API 中
|
|
|
|
|
|
|
|
|
|
|
@ -14,7 +14,7 @@ fn main() {
|
|
|
|
let one = c.get();
|
|
|
|
let one = c.get();
|
|
|
|
c.set("qwer");
|
|
|
|
c.set("qwer");
|
|
|
|
let two = c.get();
|
|
|
|
let two = c.get();
|
|
|
|
println!("{},{}", one,two);
|
|
|
|
println!("{},{}", one, two);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
@ -57,7 +57,7 @@ fn main() {
|
|
|
|
let s1 = s.borrow();
|
|
|
|
let s1 = s.borrow();
|
|
|
|
let s2 = s.borrow_mut();
|
|
|
|
let s2 = s.borrow_mut();
|
|
|
|
|
|
|
|
|
|
|
|
println!("{},{}",s1,s2);
|
|
|
|
println!("{},{}", s1, s2);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
@ -76,7 +76,7 @@ note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
|
|
|
|
|
|
|
|
|
|
|
|
而 `RefCell` 正是**用于你确信代码是正确的,而编译器却发生了误判时**。
|
|
|
|
而 `RefCell` 正是**用于你确信代码是正确的,而编译器却发生了误判时**。
|
|
|
|
|
|
|
|
|
|
|
|
对于大型的复杂程序,也可以选择使用 `RefCell` 来让事情简化。例如在 Rust 编译器的[`ctxt结构体`](https://github.com/rust-lang/rust/blob/620d1ee5346bee10ba7ce129b2e20d6e59f0377d/src/librustc/middle/ty.rs#L803-L987)中有大量的 `RefCell` 类型的 `map` 字段, 主要的原因是:这些 `map` 会被分散在各个地方的代码片段所广泛使用或修改。由于这种分散在各处的使用方式,导致了管理可变和不可变成为一件非常复杂的任务(甚至不可能),你很容易就碰到编译器抛出来的各种错误。而且 `RefCell` 的运行时错误在这种情况下也变得非常可爱:一旦有人做了不正确的使用,代码会 `panic`,然后告诉我们哪些借用冲突了。
|
|
|
|
对于大型的复杂程序,也可以选择使用 `RefCell` 来让事情简化。例如在 Rust 编译器的[`ctxt结构体`](https://github.com/rust-lang/rust/blob/620d1ee5346bee10ba7ce129b2e20d6e59f0377d/src/librustc/middle/ty.rs#L803-L987)中有大量的 `RefCell` 类型的 `map` 字段,主要的原因是:这些 `map` 会被分散在各个地方的代码片段所广泛使用或修改。由于这种分散在各处的使用方式,导致了管理可变和不可变成为一件非常复杂的任务(甚至不可能),你很容易就碰到编译器抛出来的各种错误。而且 `RefCell` 的运行时错误在这种情况下也变得非常可爱:一旦有人做了不正确的使用,代码会 `panic`,然后告诉我们哪些借用冲突了。
|
|
|
|
|
|
|
|
|
|
|
|
总之,当你确信编译器误报但不知道该如何解决时,或者你有一个引用类型,需要被四处使用和修改然后导致借用关系难以管理时,都可以优先考虑使用 `RefCell`。
|
|
|
|
总之,当你确信编译器误报但不知道该如何解决时,或者你有一个引用类型,需要被四处使用和修改然后导致借用关系难以管理时,都可以优先考虑使用 `RefCell`。
|
|
|
|
|
|
|
|
|
|
|
@ -149,7 +149,7 @@ struct MsgQueue {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Messenger for MsgQueue {
|
|
|
|
impl Messenger for MsgQueue {
|
|
|
|
fn send(&self,msg: String) {
|
|
|
|
fn send(&self, msg: String) {
|
|
|
|
self.msg_cache.push(msg)
|
|
|
|
self.msg_cache.push(msg)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
@ -229,7 +229,7 @@ RefCell { value: "我很善变,还拥有多个主人, on yeah!" }
|
|
|
|
#### 性能损耗
|
|
|
|
#### 性能损耗
|
|
|
|
相信这两者组合在一起使用时,很多人会好奇到底性能如何,下面我们来简单分析下。
|
|
|
|
相信这两者组合在一起使用时,很多人会好奇到底性能如何,下面我们来简单分析下。
|
|
|
|
|
|
|
|
|
|
|
|
首先给出一个大概的结论,这两者结合在一起使用的性能其实非常高,大致相当于没有线程安全版本的 C++ `std::shared_ptr` 指针,事实上,`C++` 这个指针的主要开销也在于原子性这个并发原语上,毕竟线程安全在哪个语言中开销都不小。
|
|
|
|
首先给出一个大概的结论,这两者结合在一起使用的性能其实非常高,大致相当于没有线程安全版本的 C++ `std::shared_ptr` 指针,事实上,C++ 这个指针的主要开销也在于原子性这个并发原语上,毕竟线程安全在哪个语言中开销都不小。
|
|
|
|
|
|
|
|
|
|
|
|
#### 内存损耗
|
|
|
|
#### 内存损耗
|
|
|
|
两者结合的数据结构与下面类似:
|
|
|
|
两者结合的数据结构与下面类似:
|
|
|
@ -252,28 +252,28 @@ struct Wrapper<T> {
|
|
|
|
#### CPU 损耗
|
|
|
|
#### CPU 损耗
|
|
|
|
从CPU来看,损耗如下:
|
|
|
|
从CPU来看,损耗如下:
|
|
|
|
|
|
|
|
|
|
|
|
- 对 `Rc<T>` 解引用是免费的(编译期),但是*带来的间接取值并不免费
|
|
|
|
- 对 `Rc<T>` 解引用是免费的(编译期),但是 `*` 带来的间接取值并不免费
|
|
|
|
- 克隆 `Rc<T>` 需要将当前的引用计数跟 `0` 和 `usize::Max` 进行一次比较,然后将计数值加1
|
|
|
|
- 克隆 `Rc<T>` 需要将当前的引用计数跟 `0` 和 `usize::Max` 进行一次比较,然后将计数值加 1
|
|
|
|
- 释放 (drop)`Rc<T>` 需要将计数值减1, 然后跟 `0` 进行一次比较
|
|
|
|
- 释放(drop) `Rc<T>` 需要将计数值减 1, 然后跟 `0` 进行一次比较
|
|
|
|
- 对 `RefCell` 进行不可变借用,需要将 `isize` 类型的借用计数加1,然后跟 `0` 进行比较
|
|
|
|
- 对 `RefCell` 进行不可变借用,需要将 `isize` 类型的借用计数加1,然后跟 `0` 进行比较
|
|
|
|
- 对 `RefCell `的不可变借用进行释放,需要将 `isize` 减1
|
|
|
|
- 对 `RefCell `的不可变借用进行释放,需要将 `isize` 减 1
|
|
|
|
- 对 `RefCell` 的可变借用大致流程跟上面差不多,但是需要先跟 `0` 比较,然后再减1
|
|
|
|
- 对 `RefCell` 的可变借用大致流程跟上面差不多,但是需要先跟 `0` 比较,然后再减 1
|
|
|
|
- 对 `RefCell` 的可变借用进行释放,需要将 `isize` 加1
|
|
|
|
- 对 `RefCell` 的可变借用进行释放,需要将 `isize` 加 1
|
|
|
|
|
|
|
|
|
|
|
|
其实这些细节不必过于关注,只要知道 `CPU` 消耗也非常低,甚至编译器还会对此进行进一步优化!
|
|
|
|
其实这些细节不必过于关注,只要知道 CPU 消耗也非常低,甚至编译器还会对此进行进一步优化!
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#### CPU 缓存 Miss
|
|
|
|
#### CPU 缓存 Miss
|
|
|
|
唯一需要担心的可能就是这种组合数据结构对于 `CPU` 缓存是否亲和,这个我们无法证明,只能提出来存在这个可能性,最终的性能影响还需要在实际场景中进行测试。
|
|
|
|
唯一需要担心的可能就是这种组合数据结构对于 CPU 缓存是否亲和,这个我们无法证明,只能提出来存在这个可能性,最终的性能影响还需要在实际场景中进行测试。
|
|
|
|
|
|
|
|
|
|
|
|
总之,分析这两者组合的性能还挺复杂的,大概总结下:
|
|
|
|
总之,分析这两者组合的性能还挺复杂的,大概总结下:
|
|
|
|
|
|
|
|
|
|
|
|
- 从表面来看,它们带来的内存和 CPU 损耗都不大
|
|
|
|
- 从表面来看,它们带来的内存和 CPU 损耗都不大
|
|
|
|
- 但是由于 `Rc` 额外的引入了一次间接取值(*),在少数场景下可能会造成性能上的显著损失
|
|
|
|
- 但是由于 `Rc` 额外的引入了一次间接取值(`*`),在少数场景下可能会造成性能上的显著损失
|
|
|
|
- CPU 缓存可能也不够亲和
|
|
|
|
- CPU 缓存可能也不够亲和
|
|
|
|
|
|
|
|
|
|
|
|
## 通过 `Cell::from_mut` 解决借用冲突
|
|
|
|
## 通过 `Cell::from_mut` 解决借用冲突
|
|
|
|
在 Rust1.37 版本中新增了两个非常实用的方法:
|
|
|
|
在 Rust 1.37 版本中新增了两个非常实用的方法:
|
|
|
|
|
|
|
|
|
|
|
|
- Cell::from_mut,该方法将 `&mut T` 转为 `&Cell<T>`
|
|
|
|
- Cell::from_mut,该方法将 `&mut T` 转为 `&Cell<T>`
|
|
|
|
- Cell::as_slice_of_cells,该方法将 `&Cell<[T]>` 转为 `&[Cell<T>]`
|
|
|
|
- Cell::as_slice_of_cells,该方法将 `&Cell<[T]>` 转为 `&[Cell<T>]`
|
|
|
|