pull/135/head
sunface 3 years ago
parent af79a02bc7
commit 9064b9fd40

@ -68,8 +68,8 @@
- [Box<T>堆对象分配](advance/smart-pointer/box.md)
- [Deref解引用](advance/smart-pointer/deref.md)
- [Drop释放资源](advance/smart-pointer/drop.md)
- [Cell与RefCell todo](advance/smart-pointer/cell.md)
- [Rc与Arc todo](advance/smart-pointer/rc-refcell.md)
- [Rc与Arc同一数据多所有者](advance/smart-pointer/rc-arc.md)
- [Cell与RefCell todo](advance/smart-pointer/cell-refcell.md)
- [自引用与内存泄漏 todo](advance/smart-pointer/self-referrence.md)
- [全局变量 todo](advance/global-variable.md)
- [多线程 todo](advance/multi-threads/intro.md)
@ -94,7 +94,7 @@
- [可变借用失败引发的深入思考](pitfalls/multiple-mutable-references.md)
- [不太勤快的迭代器](pitfalls/lazy-iterators.md)
- [奇怪的序列x..y](pitfalls/weird-ranges.md)
- [无处不在的迭代器](pitfalls/iterator-everywhere.md)
- [对抗编译检查 doing](fight-with-compiler/intro.md)
- [幽灵数据(todo)](fight-with-compiler/phantom-data.md)

@ -0,0 +1,4 @@
# Cell和RefCell
https://ryhl.io/blog/temporary-shared-mutation/

@ -1,4 +0,0 @@
# Cell todo
https://ryhl.io/blog/temporary-shared-mutation/

@ -0,0 +1,201 @@
# Rc与Arc
Rust所有权机制要求一个值只能有一个所有者在大多数情况下都没有问题但是考虑以下情况:
- 在图数据结构中,多个边可能会拥有同一个节点,该节点直到没有边指向它时,才应该被释放清理
- 在多线程中多个线程可能会指向同一个数据但是你受限于Rust的安全机制你又无法同时的可变借用该数据
以上场景不是很常见但是一旦遇到就非常棘手为了解决此类问题Rust在所有权机制之外又引入了额外的措施来简化相应的实现通过引用计数的方式允许一个数据资源在同一时刻拥有多个所有者。
这种实现机制就是`Rc`和`Arc`,前者适用于单线程,后者适用于多线程。由于二者大部分时间都是相同,因此本章将以`Rc`作为讲解主体,对于`Arc`的不同之处,也将进行单独讲解。
## Rc<T>
引用计数(reference counting),顾名思义,通过记录一个数据被引用的次数来确定该数据是否正在被使用。当引用次数归零时,就代表该数据不再被使用,因此可以被清理释放。
而`Rc`正是引用计数的英文缩写。当我们**希望在堆上分配一个对象供程序的多个部分使用且无法确定哪个部分最后一个结束时,就可以使用`Rc`成为数据值的所有者**,例如之前提到的多线程场景就非常适合。
下面是经典的所有权被转移导致报错的例子:
```rust
fn main() {
let s = String::from("hello, world");
// s在这里被转移给a
let a = Box::new(s);
// 报错此处继续尝试将s转移给b
let b = Box::new(s);
}
```
使用`Rc`就可以轻易解决:
```rust
use std::rc::Rc;
fn main() {
let a = Rc::new(String::from("hello, world"));
let b = Rc::clone(&a);
assert_eq!(2, Rc::strong_count(&a));
assert_eq!(Rc::strong_count(&a),Rc::strong_count(&b))
}
```
以上代码我们使用`Rc::new`创建了一个新的`Rc<String>`智能指针并赋给变量`a`,该指针指向底层的字符串数据。
智能指针`Rc<T>`在创建时还会将引用计数加1此时获取引用计数的关联函数`Rc::strong_count`返回的值将是`1`。
#### Rc::clone
接着,我们又使用`Rc::clone`克隆了一份智能指针`Rc<String>`,并将该智能指针的引用计数增加到`2`。
由于`a`和`b`是同一个智能指针的两个副本,因此通过它们两个获取引用计数的结果都是`2`。
不要给`clone`字样所迷惑,以为所有的`clone`都是深拷贝。这里的`clone`**仅仅复制了智能指针并增加了引用计数,并没有克隆底层数据**,因此`a`和`b`是共享了底层的字符串`s`,这种**复制效率是非常高**的。当然你也可以使用`a.clone()`的方式来克隆,但是从可读性角度,`Rc::clone`的方式我们更加推荐。
实际上Rust中还有不少`clone`都是浅拷贝,例如[迭代器的克隆](https://zhuanlan.zhihu.com/p/453149727).
#### 观察引用计数的变化
使用关联函数`Rc::strong_count`可以获取当前引用计数的值,我们来观察下引用计数如何随着变量声明、释放而变化:
```rust
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));
}
```
有几点值得注意:
- 由于变量`c`在语句块内部声明当离开语句块时它会因为超出作用域而被释放最终引用计数会减少1, 事实上这个得益于`Rc<T>`实现了`Drop`特征
- `a`,`b`,`c`三个智能指针引用计数都是同样的,并且共享底层的数据,因此打印计数时用哪个都行
- 无法看到的是: 当`a`、`b`超出作用域后引用计数会变成0最终智能指针和它指向的底层字符串都会被清理释放
#### 不可变引用
事实上,`Rc<T>`是指向底层数据的不可变的引用因此你无法通过它来修改数据这也符合Rust的借用规则要么多个不可变借用要么一个可变借用。
但是可以修改数据也是非常有用的,只不过我们需要配合其它数据类型来一起使用,例如内部可变性的`RefCell<T>`类型以及互斥锁`Mutex<T>`。事实上,在多线程编程中,`Arc`跟`Mutext`锁的组合使用非常常见,既可以让我们在不同的线程中共享数据,又允许在各个线程中对其进行修改。
#### 一个综合例子
考虑一个场景,有很多小器具,里面每个器具都有自己的主人,但是存在多个器具属于同一个主人的情况,此时使用`Rc<T>`就非常适合:
```rust
use std::rc::Rc;
struct Owner {
name: String,
// ...other fields
}
struct Gadget {
id: i32,
owner: Rc<Owner>,
// ...other fields
}
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掉其中一个智能指针引用而不是drop掉owner数据外面还有两个引用指向底层的owner数据引用计数尚未清零
// 因此owner数据依然可以被使用
println!("Gadget {} owned by {}", gadget1.id, gadget1.owner.name);
println!("Gadget {} owned by {}", gadget2.id, gadget2.owner.name);
// 在函数最后,`gadget1`和`gadget2`也被释放,最终引用计数归零,随后底层数据也被清理释放
}
```
以上代码很好的展示了`Rc<T>`的用途,当然你也可以用借用的方式,但是实现起来就会复杂的多,而且随着工具在各个代码中的四处使用,引用生命周期也将变得更加复杂,毕竟结构体中的引用类型,总是令人不那么愉快,对不?
#### Rc简单总结
- `Rc/Arc`是不可变引用,你无法修改它指向的值,只能进行读取, 如果要修改,需要配合后面章节的内部可变性`RefCell`或互斥锁`Mutex`
- 一旦最后一个拥有者消失,则资源会自动被回收,这个生命周期是在编译期就确定下来的
- Rc只能用于同一线程内部想要用于线程之间的对象共享, 你需要使用`Arc`
- `Rc<T>`是一个智能指针,实现了`Deref`特征,因此你无需先解开`Rc`指针,再使用里面的`T`,而是可以直接使用`T`, 例如上例中的`gadget1.owner.name`
## 多线程无力的Rc<T>
来看看在多线程场景使用`Rc<T>`会如何:
```rust
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`带来的多所有权机制,但是以上代码会报错:
```console
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修改起来很简单:
```rust
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`来一起使用。

@ -1,3 +1,7 @@
# 优化编译速度
https://www.reddit.com/r/rust/comments/rnkyc0/why_does_my_code_compile_faster_on_nightly/
https://www.reddit.com/r/rust/comments/rv8126/speedup_compilation_time/
https://www.reddit.com/r/rust/comments/rsfcgb/why_is_my_rust_build_so_slow/

@ -176,3 +176,7 @@ fn main() {
}
```
It takes 2.03 ms to execute :)
## 动态和静态分发
https://www.reddit.com/r/rust/comments/ruavjm/is_there_a_difference_in_performance_between/

@ -1 +1,4 @@
# performance
## How do I profile a Rust web application in production?
https://www.reddit.com/r/rust/comments/rupcux/how_do_i_profile_a_rust_web_application_in/

@ -27,3 +27,6 @@ https://www.reddit.com/r/rust/comments/rntx7s/why_use_boxleak/
## bounds check
https://www.reddit.com/r/rust/comments/rnbubh/whats_the_big_deal_with_bounds_checking/
## 使用assert 优化检查性能
https://www.reddit.com/r/rust/comments/rui1zz/write_assertions_that_clarify_code_to_both_the/

@ -0,0 +1,107 @@
# 无处不在的迭代器
Rust的迭代器无处不在直至你在它上面栽了跟头经过深入调查才发现原来是迭代器的锅。不信的话看看这个报错你能想到是迭代器的问题吗: `borrow of moved value: words`.
## 报错的代码
以下的代码非常简单,用来统计文本中字词的数量,并打印出来:
```rust
fn main() {
let s = "hello world";
let mut words = s.split(" ");
let n = words.count();
println!("{:?}",words);
}
```
四行代码,行云流水,一气呵成,且看成效:
```console
error[E0382]: borrow of moved value: `words`
--> src/main.rs:5:21
|
3 | let mut words = s.split(" ");
| --------- move occurs because `words` has type `std::str::Split<'_, &str>`, which does not implement the `Copy` trait
4 | let n = words.count();
| ------- `words` moved due to this method call
5 | println!("{:?}",words);
| ^^^^^ value borrowed here after move
```
世事难料,我以为只有的生命周期、闭包才容易背叛革命,没想到一个你浓眉大眼的`count`方法也背叛革命。从报错来看,是因为`count`方法拿走了`words`的所有权,来看看签名:
```rust
fn count(self) -> usize
```
从签名来看,编译器的报错是正确的,但是为什么?为什么一个简单的标准库`count`方法就敢拿走所有权?
## 迭代器回顾
在[迭代器](../advance/functional-programing/iterator.md#消费者与适配器)章节中,我们曾经学习过两个概念:迭代器适配器和消费者适配器,前者用于对迭代器中的元素进行操作,最终生成一个新的迭代器,例如`map`、`filter`等方法;而后者用于消费掉迭代器,最终产生一个结果,例如`collect`方法, 一个典型的示例如下:
```rust
let v1: Vec<i32> = vec![1, 2, 3];
let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();
assert_eq!(v2, vec![2, 3, 4]);
```
在其中,我们还提到一个细节,消费者适配器会拿走迭代器的所有权,那么这个是否与我们最开始碰到的问题有关系?
## 深入调查
要解释这个问题,必须要找到`words`是消费者适配器的证据,因此我们需要深入源码进行查看。
其实。。也不需要多深,只要进入`words`的源码,就能看出它属于`Iterator`特征,那说明`split`方法产生了一个迭代器?再来看看:
```rust
pub fn split<'a, P>(&'a self, pat: P) -> Split<'a, P>
where
P: Pattern<'a>,
//An iterator over substrings of this string slice, separated by characters matched by a pattern.
```
还真是,从代码注释来看,`Split`就是一个迭代器类型,用来迭代被分隔符隔开的子字符串集合。
真相大白了,`split`产生一个迭代器,而`count`方法是一个消费者适配器,用于消耗掉前者产生的迭代器,最终生成字词统计的结果。
本身问题不复杂,但是在**解决方法上,可能还有点在各位客官的意料之外**,且看下文。
## 最rusty的解决方法
你可能会想用`collect`来解决这个问题,先收集成一个集合,然后进行统计。当然此方法完全可行,但是很不`rusty`(很符合rust规范、潮流的意思),以下给出最`rusty`的解决方案:
```rust
let words = s.split(",");
let n = words.clone().count();
```
在继续之前,我得先找一个地方藏好,因为俺有一个感觉,烂西红柿正在铺天盖地的呼啸而来,伴随而来的是读者的正义呵斥:
**你管`clone`叫最好、最`rusty`的解决方法??**
大家且听我慢慢道来事实上在Rust中`clone`不总是性能低下的代名词,因为`clone`的行为完全取决于它的具体实现。
#### 迭代器的`clone`代价
对于迭代器而言,它其实并不需要持有数据才能进行迭代,事实上它包含一个引用,该引用指向了保存在堆上的数据,而迭代器自身的结构是保存在栈上。
因此对迭代器的`clone`仅仅是复制了一份栈上的简单结构,性能非常高效,例如:
```rust
pub struct Split<'a, T: 'a, P>
where
P: FnMut(&T) -> bool,
{
// Used for `SplitWhitespace` and `SplitAsciiWhitespace` `as_str` methods
pub(crate) v: &'a [T],
pred: P,
// Used for `SplitAsciiWhitespace` `as_str` method
pub(crate) finished: bool,
}
impl<T, P> Clone for Split<'_, T, P>
where
P: Clone + FnMut(&T) -> bool,
{
fn clone(&self) -> Self {
Split { v: self.v, pred: self.pred.clone(), finished: self.finished }
}
}
```
以上代码实现了对`Split`迭代器的克隆,可以看出,底层的的数组`self.v`并没有被克隆而是简单的复制了一个引用,依然指向了底层的数组`&[T]`,因此这个克隆非常高效。
## 总结
看起来是无效借用导致的错误实际上是迭代器被消费了导致的问题这说明Rust编译器虽然会告诉你错误原因但是这个原因不总是根本原因。我们需要一双慧眼和勤劳的手来挖掘出这个宝藏最后为己所用。
同时克隆在Rust中也并不总是**bad guy**的代名词,有的时候我们可以大胆去使用,当然前提是了解你的代码场景和具体的`clone`实现,这样你也能像文中那样作出非常`rusty`的选择。

@ -8,3 +8,6 @@ https://www.reddit.com/r/rust/comments/rrgho1/what_is_the_recommended_way_to_use
## 最佳开发流程
cargo watch
## 测试文件组织结构
https://www.reddit.com/r/rust/comments/rsuhnn/need_a_piece_of_advice_about_organising_tests/

@ -0,0 +1 @@
https://www.reddit.com/r/rust/comments/rtqrx4/introducing_atomicstory/

@ -25,3 +25,6 @@ fn main() {
}
}
```
## 多个线程同时无锁的对一个数组进行修改
https://www.reddit.com/r/rust/comments/rtutr0/lockless_threads_for_mutable_operations/
Loading…
Cancel
Save