|
|
@ -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`。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## 总结
|
|
|
|
## 总结
|
|
|
|