Update: unified format

pull/462/head
Allan Downey 3 years ago
parent 9607c9eff4
commit f098e33074

@ -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>]`

@ -32,7 +32,7 @@ fn main() {
let b = Rc::clone(&a); let b = Rc::clone(&a);
assert_eq!(2, Rc::strong_count(&a)); assert_eq!(2, Rc::strong_count(&a));
assert_eq!(Rc::strong_count(&a),Rc::strong_count(&b)) assert_eq!(Rc::strong_count(&a), Rc::strong_count(&b))
} }
``` ```
@ -114,12 +114,15 @@ fn main() {
drop(gadget_owner); drop(gadget_owner);
// 尽管在上面我们释放了 gadget_owner但是依然可以在这里使用 owner 的信息 // 尽管在上面我们释放了 gadget_owner但是依然可以在这里使用 owner 的信息
// 原因是在 drop 之前,存在三个指向 Gadget Man 的智能指针引用,上面仅仅 drop 掉其中一个智能指针引用,而不是 drop 掉 owner 数据,外面还有两个引用指向底层的 owner 数据,引用计数尚未清零 // 原因是在 drop 之前,存在三个指向 Gadget Man 的智能指针引用,上面仅仅
// drop 掉其中一个智能指针引用,而不是 drop 掉 owner 数据,外面还有两个
// 引用指向底层的 owner 数据,引用计数尚未清零
// 因此 owner 数据依然可以被使用 // 因此 owner 数据依然可以被使用
println!("Gadget {} owned by {}", gadget1.id, gadget1.owner.name); println!("Gadget {} owned by {}", gadget1.id, gadget1.owner.name);
println!("Gadget {} owned by {}", gadget2.id, gadget2.owner.name); println!("Gadget {} owned by {}", gadget2.id, gadget2.owner.name);
// 在函数最后,`gadget1` 和 `gadget2` 也被释放,最终引用计数归零,随后底层数据也被清理释放 // 在函数最后,`gadget1` 和 `gadget2` 也被释放,最终引用计数归零,随后底层
// 数据也被清理释放
} }
``` ```
@ -145,7 +148,7 @@ fn main() {
for _ in 0..10 { for _ in 0..10 {
let s = Rc::clone(&s); let s = Rc::clone(&s);
let handle = thread::spawn(move || { let handle = thread::spawn(move || {
println!("{}",s) println!("{}", s)
}); });
} }
} }
@ -182,7 +185,7 @@ fn main() {
for _ in 0..10 { for _ in 0..10 {
let s = Arc::clone(&s); let s = Arc::clone(&s);
let handle = thread::spawn(move || { let handle = thread::spawn(move || {
println!("{}",s) println!("{}", s)
}); });
} }
} }

Loading…
Cancel
Save