Update rc-arc.md

pull/360/head
Jesse 3 years ago committed by GitHub
parent 62546f9cb9
commit 1263a7daa3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,8 +1,8 @@
# Rc 与 Arc # Rc 与 Arc
Rust所有权机制要求一个值只能有一个所有者在大多数情况下都没有问题但是考虑以下情况: Rust 所有权机制要求一个值只能有一个所有者,在大多数情况下,都没有问题,但是考虑以下情况
- 在图数据结构中,多个边可能会拥有同一个节点,该节点直到没有边指向它时,才应该被释放清理 - 在图数据结构中,多个边可能会拥有同一个节点,该节点直到没有边指向它时,才应该被释放清理
- 在多线程中多个线程可能会持有同一个数据但是你受限于Rust的安全机制无法同时获取该数据的可变引用 - 在多线程中,多个线程可能会持有同一个数据,但是你受限于 Rust 的安全机制,无法同时获取该数据的可变引用
以上场景不是很常见但是一旦遇到就非常棘手为了解决此类问题Rust 在所有权机制之外又引入了额外的措施来简化相应的实现:通过引用计数的方式,允许一个数据资源在同一时刻拥有多个所有者。 以上场景不是很常见但是一旦遇到就非常棘手为了解决此类问题Rust 在所有权机制之外又引入了额外的措施来简化相应的实现:通过引用计数的方式,允许一个数据资源在同一时刻拥有多个所有者。
@ -45,9 +45,9 @@ fn main() {
由于 `a``b` 是同一个智能指针的两个副本,因此通过它们两个获取引用计数的结果都是 `2` 由于 `a``b` 是同一个智能指针的两个副本,因此通过它们两个获取引用计数的结果都是 `2`
不要给`clone`字样所迷惑,以为所有的`clone`都是深拷贝。这里的`clone`**仅仅复制了智能指针并增加了引用计数,并没有克隆底层数据**,因此`a`和`b`是共享了底层的字符串`s`,这种**复制效率是非常高**的。当然你也可以使用`a.clone()`的方式来克隆,但是从可读性角度,`Rc::clone`的方式我们更加推荐 不要`clone` 字样所迷惑,以为所有的 `clone` 都是深拷贝。这里的 `clone` **仅仅复制了智能指针并增加了引用计数,并没有克隆底层数据**,因此 `a` `b` 是共享了底层的字符串 `s`,这种**复制效率是非常高**的。当然你也可以使用 `a.clone()` 的方式来克隆,但是从可读性角度,我们更加推荐 `Rc::clone` 的方式。
实际上Rust中还有不少`clone`都是浅拷贝,例如[迭代器的克隆](https://course.rs/pitfalls/iterator-everywhere.html). 实际上Rust 中,还有不少 `clone` 都是浅拷贝,例如[迭代器的克隆](https://course.rs/pitfalls/iterator-everywhere.html)
#### 观察引用计数的变化 #### 观察引用计数的变化
使用关联函数 `Rc::strong_count` 可以获取当前引用计数的值,我们来观察下引用计数如何随着变量声明、释放而变化: 使用关联函数 `Rc::strong_count` 可以获取当前引用计数的值,我们来观察下引用计数如何随着变量声明、释放而变化:
@ -68,18 +68,18 @@ fn main() {
有几点值得注意: 有几点值得注意:
- 由于变量`c`在语句块内部声明,当离开语句块时它会因为超出作用域而被释放,最终引用计数会减少1, 事实上这个得益于`Rc<T>`实现了`Drop`特征 - 由于变量 `c` 在语句块内部声明,当离开语句块时它会因为超出作用域而被释放,所以引用计数会减少1事实上这个得益于 `Rc<T>` 实现了 `Drop` 特征
- `a`,`b`,`c`三个智能指针引用计数都是同样的,并且共享底层的数据,因此打印计数时用哪个都行 - `a`、`b`、`c` 三个智能指针引用计数都是同样的,并且共享底层的数据,因此打印计数时用哪个都行
- 无法看到的是: 当`a`、`b`超出作用域后,引用计数会变成0最终智能指针和它指向的底层字符串都会被清理释放 - 无法看到的是:当 `a`、`b` 超出作用域后,引用计数会变成 0最终智能指针和它指向的底层字符串都会被清理释放
#### 不可变引用 #### 不可变引用
事实上,`Rc<T>`是指向底层数据的不可变的引用因此你无法通过它来修改数据这也符合Rust的借用规则要么多个不可变借用要么一个可变借用。 事实上,`Rc<T>` 是指向底层数据的不可变的引用,因此你无法通过它来修改数据,这也符合 Rust 的借用规则:要么存在多个不可变借用,要么只能存在一个可变借用。
但是可以修改数据也是非常有用的,只不过我们需要配合其它数据类型来一起使用,例如内部可变性的`RefCell<T>`类型以及互斥锁`Mutex<T>`。事实上,在多线程编程中,`Arc`跟`Mutext`锁的组合使用非常常见,它们既可以让我们在不同的线程中共享数据,又允许在各个线程中对其进行修改。 但是实际开发中我们往往需要对数据进行修改,这时单独使用 `Rc<T>` 无法满足我们的需求,需要配合其它数据类型来一起使用,例如内部可变性的 `RefCell<T>` 类型以及互斥锁 `Mutex<T>`。事实上,在多线程编程中,`Arc` `Mutext` 锁的组合使用非常常见,它们既可以让我们在不同的线程中共享数据,又允许在各个线程中对其进行修改。
#### 一个综合例子 #### 一个综合例子
考虑一个场景,有很多小器具,里面每个器具都有自己的主人,但是存在多个器具属于同一个主人的情况,此时使用`Rc<T>`就非常适合: 考虑一个场景,有很多小工具,每个工具都有自己的主人,但是存在多个工具属于同一个主人的情况,此时使用 `Rc<T>` 就非常适合:
```rust ```rust
use std::rc::Rc; use std::rc::Rc;
@ -96,11 +96,9 @@ struct Gadget {
fn main() { fn main() {
// 创建一个基于引用计数的 `Owner`. // 创建一个基于引用计数的 `Owner`.
let gadget_owner: Rc<Owner> = Rc::new( let gadget_owner: Rc<Owner> = Rc::new(Owner {
Owner {
name: "Gadget Man".to_string(), name: "Gadget Man".to_string(),
} });
);
// 创建两个不同的工具,它们属于同一个主人 // 创建两个不同的工具,它们属于同一个主人
let gadget1 = Gadget { let gadget1 = Gadget {
@ -115,8 +113,8 @@ fn main() {
// 释放掉第一个 `Rc<Owner>` // 释放掉第一个 `Rc<Owner>`
drop(gadget_owner); drop(gadget_owner);
// 尽管在之前我们释放了gadget_owner但是依然可以在这里使用owner的信息 // 尽管在上面我们释放了 gadget_owner但是依然可以在这里使用 owner 的信息
// 原因是上面仅仅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);
@ -125,19 +123,19 @@ fn main() {
} }
``` ```
以上代码很好的展示了`Rc<T>`的用途,当然你也可以用借用的方式,但是实现起来就会复杂的多,而且随着`Gadget`在代码的各个地方使用,引用生命周期也将变得更加复杂,毕竟结构体中的引用类型,总是令人不那么愉快,对不? 以上代码很好的展示了 `Rc<T>` 的用途,当然你也可以用借用的方式,但是实现起来就会复杂得多,而且随着 `Gadget` 在代码的各个地方使用,引用生命周期也将变得更加复杂,毕竟结构体中的引用类型,总是令人不那么愉快,对不?
#### Rc 简单总结 #### Rc 简单总结
- `Rc/Arc`是不可变引用,你无法修改它指向的值,只能进行读取, 如果要修改,需要配合后面章节的内部可变性`RefCell`或互斥锁`Mutex` - `Rc/Arc` 是不可变引用,你无法修改它指向的值,只能进行读取如果要修改,需要配合后面章节的内部可变性 `RefCell` 或互斥锁 `Mutex`
- 一旦最后一个拥有者消失,则资源会自动被回收,这个生命周期是在编译期就确定下来的 - 一旦最后一个拥有者消失,则资源会自动被回收,这个生命周期是在编译期就确定下来的
- Rc只能用于同一线程内部想要用于线程之间的对象共享, 你需要使用`Arc` - `Rc` 只能用于同一线程内部,想要用于线程之间的对象共享,你需要使用 `Arc`
- `Rc<T>`是一个智能指针,实现了`Deref`特征,因此你无需先解开`Rc`指针,再使用里面的`T`,而是可以直接使用`T`, 例如上例中的`gadget1.owner.name` - `Rc<T>` 是一个智能指针,实现了 `Deref` 特征,因此你无需先解开 `Rc` 指针,再使用里面的 `T`,而是可以直接使用 `T`,例如上例中的 `gadget1.owner.name`
## 多线程无力的 Rc<T> ## 多线程无力的 Rc<T>
来看看在多线程场景使用`Rc<T>`会如何: 来看看在多线程场景使用 `Rc<T>` 会如何:
```rust ```rust
use std::rc::Rc; use std::rc::Rc;
use std::thread; use std::thread;
@ -155,26 +153,26 @@ fn main() {
由于我们还没有学习多线程的章节,上面的例子就特地简化了相关的实现。首先通过 `thread::spawn` 创建一个线程,然后使用 `move` 关键字把克隆出的 `s` 的所有权转移到线程中。 由于我们还没有学习多线程的章节,上面的例子就特地简化了相关的实现。首先通过 `thread::spawn` 创建一个线程,然后使用 `move` 关键字把克隆出的 `s` 的所有权转移到线程中。
能够实现这一点,完全得益于`Rc`带来的多所有权机制,但是以上代码会报错: 能够实现这一点,完全得益于 `Rc` 带来的多所有权机制,但是以上代码会报错
```console ```console
error[E0277]: `Rc<String>` cannot be sent between threads safely error[E0277]: `Rc<String>` cannot be sent between threads safely
``` ```
表面原因是 `Rc<T>` 不能在线程间安全的传递,实际上是因为它没有实现 `Send` 特征,而该特征是恰恰是多线程间传递数据的关键,我们会在多线程章节中进行讲解。 表面原因是 `Rc<T>` 不能在线程间安全的传递,实际上是因为它没有实现 `Send` 特征,而该特征是恰恰是多线程间传递数据的关键,我们会在多线程章节中进行讲解。
当然,还有更深层的原因: 由于`Rc<T>`需要管理引用计数,但是该计数器并没有使用任何并发原语,因此无法实现原子化的计数操作, 最终会导致计数错误。 当然,还有更深层的原因:由于 `Rc<T>` 需要管理引用计数,但是该计数器并没有使用任何并发原语,因此无法实现原子化的计数操作最终会导致计数错误。
好在天无绝人之路,一起来看看Rust为我们提供的功能一致但是多线程安全的`Arc` 好在天无绝人之路,一起来看看 Rust 为我们提供的功能类似但是多线程安全的 `Arc`
## Arc ## Arc
`Arc``Atomic Rc` 的缩写,顾名思义:原子化的 `Rc<T>` 智能指针。原子化是一种并发原语,我们在后续章节会进行深入讲解,这里你只要知道它能保证我们的数据能够安全的在线程间共享即可。 `Arc``Atomic Rc` 的缩写,顾名思义:原子化的 `Rc<T>` 智能指针。原子化是一种并发原语,我们在后续章节会进行深入讲解,这里你只要知道它能保证我们的数据能够安全的在线程间共享即可。
#### Arc 的性能损耗 #### Arc 的性能损耗
你可能好奇,为何不直接使用`Arc`,还要画蛇添足弄一个`Rc`还有Rust的基本数据类型、标准库数据类型为什么不自动实现原子化操作 你可能好奇,为何不直接使用 `Arc`,还要画蛇添足弄一个 `Rc`,还有 Rust 的基本数据类型、标准库数据类型为什么不自动实现原子化操作?这样就不存在线程不安全的问题了。
原因在于原子化或者其它锁带来的线程安全,都会伴随着性能损耗,而且这种性能损耗还不小因此Rust把这种选择权交给你毕竟需要线程安全的代码其实占比并不高大部分时间我们都在跟线程内的代码执行打交道 原因在于原子化或者其它锁虽然可以带来的线程安全,但是都会伴随着性能损耗,而且这种性能损耗还不小。因此 Rust 把这种选择权交给你,毕竟需要线程安全的代码其实占比并不高,大部分时候我们开发的程序都在一个线程内
`Arc`和`Rc`拥有完全一样的API修改起来很简单: `Arc``Rc` 拥有完全一样的 API修改起来很简单
```rust ```rust
use std::sync::Arc; use std::sync::Arc;
use std::thread; use std::thread;
@ -190,7 +188,7 @@ fn main() {
} }
``` ```
对了,两者还有一点区别: `Arc`和`Rc`并没有定义在同一个模块,前者通过`use std::sync::Arc`来引入,后者`use std::rc::Rc`. 对了,两者还有一点区别`Arc` 和 `Rc` 并没有定义在同一个模块,前者通过 `use std::sync::Arc` 来引入,后者通过 `use std::rc::Rc`
## 总结 ## 总结

Loading…
Cancel
Save