update readme.md

pull/155/head
sunface 3 years ago
parent 20ab5649e0
commit c6fb4c9d3e

@ -73,13 +73,13 @@
- [循环引用与自引用](advance/circle-self-ref/intro.md) - [循环引用与自引用](advance/circle-self-ref/intro.md)
- [Weak与循环引用](advance/circle-self-ref/circle-reference.md) - [Weak与循环引用](advance/circle-self-ref/circle-reference.md)
- [结构体中的自引用](advance/circle-self-ref/self-referential.md)) - [结构体中的自引用](advance/circle-self-ref/self-referential.md))
- [多线程并发编程 todo](advance/concurrency-with-threads/intro.md) - [多线程并发编程 doing](advance/concurrency-with-threads/intro.md)
- [并发和并行](advance/concurrency-with-threads/concurrency-parallelism.md) - [并发和并行](advance/concurrency-with-threads/concurrency-parallelism.md)
- [线程管理 todo](advance/concurrency-with-threads/thread.md) - [使用线程](advance/concurrency-with-threads/thread.md)
- [消息传递 todo](advance/concurrency-with-threads/message-passing.md) - [消息传递 todo](advance/concurrency-with-threads/message-passing.md)
- [数据共享Mutex、Rwlock todo](advance/concurrency-with-threads/ref-counter-lock.md) - [数据共享Mutex、Rwlock todo](advance/concurrency-with-threads/ref-counter-lock.md)
- [数据竞争 todo](advance/concurrency-with-threads/races.md)
- [Send、Sync todo](advance/multi-threads/send-sync.md) - [Send、Sync todo](advance/multi-threads/send-sync.md)
- [一个综合例子](advance/multi-threads/example.md)
- [async/await并发编程 todo](advance/async/intro.md) - [async/await并发编程 todo](advance/async/intro.md)
- [async/await语法 todo](advance/async/async-await.md) - [async/await语法 todo](advance/async/async-await.md)
- [future详解 todo](advance/async/future/into.md) - [future详解 todo](advance/async/future/into.md)

@ -53,9 +53,20 @@
相信你已经能够得出结论——**“并行”概念是“并发”概念的一个子集**。也就是说,你可以编写一个拥有多个线程或者进程的并发程序,但如果没有多核处理器来执行这个程序,那么就不能以并行方式来运行代码。因此,凡是在求解单个问题时涉及多个执行流程的编程模式或者执行行为,都属于并发编程的范畴。 相信你已经能够得出结论——**“并行”概念是“并发”概念的一个子集**。也就是说,你可以编写一个拥有多个线程或者进程的并发程序,但如果没有多核处理器来执行这个程序,那么就不能以并行方式来运行代码。因此,凡是在求解单个问题时涉及多个执行流程的编程模式或者执行行为,都属于并发编程的范畴。
## 本书的选择 ## 编程语言的并发模型
由于对于编程而言,并行度是无法控制的,我们能控制的仅仅是并发,因此出于简单性的原则,在本书中,我们将统一使用并发来描述问题,除非因为某些特定场景需要时,才进行并发和并行的区分。 如果大家学过其它语言的多线程,可能就知道不同语言对于线程的实现可能大相径庭:
- 由于操作系统提供了创建线程的API因此部分语言会直接调用该API来创建线程因此最终程序内的线程数和该程序占用的操作系统线程数相等一般称之为**1:1线程模型**例如Rust
- 还有些语言在内部实现了自己的线程模型(绿色线程、协程)程序内部的M个线程最后会以某种映射方式使用N个操作系统线程去运行因此称之为**M:N线程模型**, 其中M和N并没有特定的彼此限制关系。一个典型的代表就是Go语言
- 还有些语言使用了Actor模型基于消息传递进行并发, 例如Erlang语言
总之每一种模型都有其优缺点及选择上的权衡而Rust在设计时考虑的权衡就是运行时(runtime)。出于Rust的系统级使用场景且要保证调用C时的极致性能它最终选择了尽量小的运行时实现。
> 运行时是那些会被打包到所有程序可执行文件中的Rust代码根据每个语言的设计权衡运行时虽然有大有小(例如Go语言由于实现了协程和GC运行时相对就会更大一些),但是除了汇编之外,每个语言都拥有它。小运行时的其中一个好处在于最终编译出的可执行文件会相对较小,同时也让该语言更容易被其它语言引入使用。
而绿色线程/协程的实现会显著增大运行时的大小因此Rust只在标准库中提供了`1:1`的线程模型,如果你愿意牺牲一些性能来换取更精确的线程控制以及更小的线程上下文切换成本,那么可以选择`Rust`中的`M:N`模型,这些模型由三方库提供了实现,例如大名鼎鼎的`tokio`。
在了解了并发和并行后我们可以正式开始Rust的多线程之旅。

@ -1 +1,221 @@
# 线程管理(todo) # 使用线程
放在十年前多线程编程可能还是一个少数人才掌握的核心概念而在今天随着编程语言的不断发展多线程、多协程、Actor等并发编程方式已经深入人心同时门槛也在不断降低本章节我们来看看在Rust中该如何使用多线程。
由于多线程的代码是同时运行的,因此我们无法保证线程间的执行顺序,这会导致一些问题:
- 竞态条件(race conditions), 多个线程以非一致性的顺序同时访问数据资源
- 死锁(deadlocks),两个线程都想使用某个资源,但是又都在等待对方释放资源后才能使用,结果最终都无法继续执行
- 一些因为多线程导致的很隐晦的BUG且难以复现和解决
虽然Rust已经通过各种机制减少了上述情况的发生但是依然无法完全避免上述情况因此我们在编程时需要格外的小心同时本书也会列出多线程编程时常见的陷阱让你提前规避可能的风险。
## 创建线程
使用`thread::spawn`可以创建线程:
```rust
use std::thread;
use std::time::Duration;
fn main() {
thread::spawn(|| {
for i in 1..10 {
println!("hi number {} from the spawned thread!", i);
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..5 {
println!("hi number {} from the main thread!", i);
thread::sleep(Duration::from_millis(1));
}
}
```
有几点值得注意:
- 线程内部的代码使用闭包来执行
- `main`线程一旦结束,程序就立刻结束,因此需要保持它的存活,直到其它子线程完成自己的任务
- `thread::sleep`会让当前线程休眠指定的时间,随后其它线程会被调度运行(上一节并发与并行中有简单介绍过)因此就算你的电脑只有一个CPU核心该程序也会如同多CPU核心般的完成这就是并发
来看看输出:
```console
hi number 1 from the main thread!
hi number 1 from the spawned thread!
hi number 2 from the main thread!
hi number 2 from the spawned thread!
hi number 3 from the main thread!
hi number 3 from the spawned thread!
hi number 4 from the spawned thread!
hi number 4 from the main thread!
hi number 5 from the spawned thread!
```
如果多运行几次,你会发现好像每次输出会不太一样,因为: 虽说线程往往是轮流执行的,但是这一点无法被保证!这个依赖于你的操作系统如何调度这些线程。总之,**千万不要依赖线程的执行顺序**!
## 等待所有线程的完成
上面的代码你不仅无法让子线程打印到10因为主线程会提前结束导致子线程也随之结束更过分的是如果当前系统繁忙甚至该子线程还没被创建主线程就已经结束了
因此我们需要一个方法让主线程安全、可靠的等所有子线程完成任务后再kill self:
```rust
use std::thread;
use std::time::Duration;
fn main() {
let handle = thread::spawn(|| {
for i in 1..5 {
println!("hi number {} from the spawned thread!", i);
thread::sleep(Duration::from_millis(1));
}
});
handle.join().unwrap();
for i in 1..5 {
println!("hi number {} from the main thread!", i);
thread::sleep(Duration::from_millis(1));
}
}
```
通过调用`handle.join`,可以让当前线程阻塞,直到它等待的子线程的结束,在上面代码中,由于`main`线程会被阻塞,因此它直到子线程结束后才会输出自己的`1..5`:
```console
hi number 1 from the spawned thread!
hi number 2 from the spawned thread!
hi number 3 from the spawned thread!
hi number 4 from the spawned thread!
hi number 1 from the main thread!
hi number 2 from the main thread!
hi number 3 from the main thread!
hi number 4 from the main thread!
```
以上输出清晰的展示了线程阻塞的作用,同时如果你将`handle.join`放置到`main`线程中的`for`循环后面,那就是另外一个结果:两个线程交替输出。
## 在线程闭包中使用move
在[闭包章节](../../advance/functional-programing/closure.md#move和Fn)中,有讲过`move`关键字在闭包中的使用可以让该闭包拿走环境中某个值的所有权,同样的,你可以使用`move`来将所有权从一个线程转移到另外一个线程。
首先,来看看在一个线程中直接使用另一个线程中的数据会如何:
```rust
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(|| {
println!("Here's a vector: {:?}", v);
});
handle.join().unwrap();
}
```
以上代码在子线程的闭包中捕获了环境中的`v`变量,来看看结果:
```console
error[E0373]: closure may outlive the current function, but it borrows `v`, which is owned by the current function
--> src/main.rs:6:32
|
6 | let handle = thread::spawn(|| {
| ^^ may outlive borrowed value `v`
7 | println!("Here's a vector: {:?}", v);
| - `v` is borrowed here
|
note: function requires argument type to outlive `'static`
--> src/main.rs:6:18
|
6 | let handle = thread::spawn(|| {
| __________________^
7 | | println!("Here's a vector: {:?}", v);
8 | | });
| |______^
help: to force the closure to take ownership of `v` (and any other referenced variables), use the `move` keyword
|
6 | let handle = thread::spawn(move || {
| ++++
```
其实代码本身并没有什么问题问题在于Rust无法确定新的线程会活多久(多个线程的结束顺序并不是固定的),所以也无法确定新线程所引用的`v`是否在使用过程中一直合法:
```rust
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(|| {
println!("Here's a vector: {:?}", v);
});
drop(v); // oh no!
handle.join().unwrap();
}
```
大家要记住,线程的启动时间点和结束时间点是不确定的,因此假设上述代码可以正常运行,那么当`v`被释放掉时,新的线程很可能还没有结束甚至还没有被创建成功,此时新线程对`v`的引用立刻就不再合法!
好在报错里进行了提示:`to force the closure to take ownership of `v` (and any other referenced variables), use the `move` keyword`,让我们使用`move`关键字拿走`v`的所有权即可:
```rust
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(move || {
println!("Here's a vector: {:?}", v);
});
handle.join().unwrap();
// 下面代码会报错borrow of moved value: `v`
// println!("{:?}",v);
}
```
如上所示很简单的代码而且Rust的所有权机制保证了数据使用上的安全`v`的所有权被转移给新的线程后,`main`线程将无法继续使用:最后一行代码将报错。
## 线程是如何结束的
之前我们提到`main`线程是程序的主线程,一旦结束,则程序随之结束,同时各个子线程也将被强行终止。那么有一个问题,如果不是`main`线程,那么父线程的结束会导致什么?自生自灭还是被干掉?
在系统编程中操作系统提供了直接杀死线程的接口简单粗暴但是Rust并没有提供这样的接口原因在于粗暴地终止一个线程可能会导致资源没有释放、状态混乱等不可预期的结果一向以安全自称的Rust, 自然不会砸自己的饭碗。
那么Rust中线程是如何结束的呢答案很简单线程的代码执行完线程就会自动结束。但是如果线程中的代码不会执行完呢这种情况分为两种:
- 线程的任务是一个循环IO读取任务流程类似: IO阻塞等待读取新的数据 -> 读到数据,处理完成 -> 继续阻塞等待 ··· -> 收到socket关闭的信号 -> 结束线程, 在此过程中绝大部分时间线程都处于阻塞的状态因此虽然看上去是循环CPU占用其实很小也是网络服务中最最常见的模型
- 线程的任务是一个循环里面没有任何阻塞包括休眠这种操作也没有此时cpu很不幸的会被跑满而且你如果没有设置终止条件该线程将持续跑满一个cpu核心, 并且不会被终止,直到`main`线程的结束
第一情况很常见,我们来模拟看看第二种情况:
```rust
use std::thread;
use std::time::Duration;
fn main() {
// 创建一个线程
let new_thread = thread::spawn(move || {
// 再创建一个线程
thread::spawn(move || {
loop {
println!("I am a new thread.");
}
})
});
// 等待新创建的线程执行完成
new_thread.join().unwrap();
println!("Child thread is finish!");
// 睡眠一段时间,看子线程创建的子线程是否还在运行
thread::sleep(Duration::from_millis(100));
}
```
以上代码中,`main`线程创建了一个新的线程A同时该新线程又创建了一个新的线程`B`,可以看到`A`线程在创建完`B`线程后就立即结束了,而`B`线程则在不停的循环输出。
从之前的线程结束规则,我们可以猜测程序将这样执行:`A`线程结束后,由它创建的`B`线程仍在疯狂输出,直到`main`线程在100毫秒后结束。如果你把该时间增加到几十秒就可以看到你的CPU核心100%的盛况了--
## 总结
[Rust的线程模型](./intro.md)是`1:1`模型因为Rust要保持尽量小的运行时。
我们可以使用`thread::spawn`来创建线程,创建出的多个线程之间并不存在执行顺序关系,因此代码逻辑千万不要依赖于线程间的执行顺序。
`main`线程若是结束,则所有子线程都将被终止,如果希望等待子线程结束后,再结束`main`线程,你需要使用创建线程时返回的句柄的`join`方法。
在线程中无法直接借用外部环境中的变量值因为新线程的启动时间点和结束时间点是不确定的这样Rust就无法保证该线程中借用的变量在使用过程中依然是合法的。你可以使用`move`关键字将变量的所有权转移给新的线程,来解决此问题。
父线程结束后,子线程仍在持续运行,直到子线程的代码运行完成或者`main`线程的结束。

@ -0,0 +1,3 @@
# 一个综合例子
https://nickymeuleman.netlify.app/blog/multithreading-rust
Loading…
Cancel
Save