pull/157/head
sunface 3 years ago
parent bb214077e4
commit 0d30af30ed

@ -1,6 +1,7 @@
# 使用线程
放在十年前多线程编程可能还是一个少数人才掌握的核心概念而在今天随着编程语言的不断发展多线程、多协程、Actor等并发编程方式已经深入人心同时门槛也在不断降低本章节我们来看看在Rust中该如何使用多线程。
放在十年前多线程编程可能还是一个少数人才掌握的核心概念而在今天随着编程语言的不断发展多线程、多协程、Actor等并发编程方式已经深入人心同时多线程编程的门槛也在不断降低本章节我们来看看在Rust中该如何使用多线程。
## 多线程编程的风险
由于多线程的代码是同时运行的,因此我们无法保证线程间的执行顺序,这会导致一些问题:
- 竞态条件(race conditions), 多个线程以非一致性的顺序同时访问数据资源
@ -33,7 +34,7 @@ fn main() {
有几点值得注意:
- 线程内部的代码使用闭包来执行
- `main`线程一旦结束,程序就立刻结束,因此需要保持它的存活,直到其它子线程完成自己的任务
- `thread::sleep`会让当前线程休眠指定的时间,随后其它线程会被调度运行(上一节并发与并行中有简单介绍过)因此就算你的电脑只有一个CPU核心该程序也会如同多CPU核心般的完成,这就是并发!
- `thread::sleep`会让当前线程休眠指定的时间,随后其它线程会被调度运行(上一节并发与并行中有简单介绍过)因此就算你的电脑只有一个CPU核心该程序也会表现的如同多CPU核心般,这就是并发!
来看看输出:
```console
@ -48,9 +49,9 @@ hi number 4 from the main thread!
hi number 5 from the spawned thread!
```
如果多运行几次,你会发现好像每次输出会不太一样,因为: 虽说线程往往是轮流执行的,但是这一点无法被保证!这个依赖于你的操作系统如何调度这些线程。总之,**千万不要依赖线程的执行顺序**!
如果多运行几次,你会发现好像每次输出会不太一样,因为: 虽说线程往往是轮流执行的,但是这一点无法被保证!线程调度的方式往往取决于你使用的操作系统。总之,**千万不要依赖线程的执行顺序**!
## 等待所有线程的完成
## 等待子线程的结束
上面的代码你不仅无法让子线程打印到10因为主线程会提前结束导致子线程也随之结束更过分的是如果当前系统繁忙甚至该子线程还没被创建主线程就已经结束了
因此我们需要一个方法让主线程安全、可靠的等所有子线程完成任务后再kill self:
@ -87,7 +88,7 @@ hi number 3 from the main thread!
hi number 4 from the main thread!
```
以上输出清晰的展示了线程阻塞的作用,同时如果你将`handle.join`放置`main`线程中的`for`循环后面,那就是另外一个结果:两个线程交替输出。
以上输出清晰的展示了线程阻塞的作用,同时如果你将`handle.join`放置`main`线程中的`for`循环后面,那就是另外一个结果:两个线程交替输出。
## 在线程闭包中使用move
在[闭包章节](../../advance/functional-programing/closure.md#move和Fn)中,有讲过`move`关键字在闭包中的使用可以让该闭包拿走环境中某个值的所有权,同样的,你可以使用`move`来将所有权从一个线程转移到另外一个线程。
@ -149,7 +150,7 @@ fn main() {
```
大家要记住,线程的启动时间点和结束时间点是不确定的,因此假设上述代码可以正常运行,那么当`v`被释放掉时,新的线程很可能还没有结束甚至还没有被创建成功,此时新线程对`v`的引用立刻就不再合法!
好在报错里进行了提示:`to force the closure to take ownership of `v` (and any other referenced variables), use the `move` keyword`,让我们使用`move`关键字拿走`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;
@ -174,7 +175,7 @@ fn main() {
在系统编程中操作系统提供了直接杀死线程的接口简单粗暴但是Rust并没有提供这样的接口原因在于粗暴地终止一个线程可能会导致资源没有释放、状态混乱等不可预期的结果一向以安全自称的Rust, 自然不会砸自己的饭碗。
那么Rust中线程是如何结束的呢答案很简单线程的代码执行完线程就会自动结束。但是如果线程中的代码不会执行完呢这种情况分为两种:
那么Rust中线程是如何结束的呢答案很简单线程的代码执行完线程就会自动结束。但是如果线程中的代码不会执行完呢那么情况可以分为两种进行讨论:
- 线程的任务是一个循环IO读取任务流程类似: IO阻塞等待读取新的数据 -> 读到数据,处理完成 -> 继续阻塞等待 ··· -> 收到socket关闭的信号 -> 结束线程, 在此过程中绝大部分时间线程都处于阻塞的状态因此虽然看上去是循环CPU占用其实很小也是网络服务中最最常见的模型
- 线程的任务是一个循环里面没有任何阻塞包括休眠这种操作也没有此时cpu很不幸的会被跑满而且你如果没有设置终止条件该线程将持续跑满一个cpu核心, 并且不会被终止,直到`main`线程的结束
@ -212,7 +213,7 @@ fn main() {
下面我们从多个方面来看看多线程的性能大概是怎么样的。
#### 创建线程的性能
据不精确估算创建一个线程大概需要0.24毫秒,随着线程快速创建时,这个值会变得更大,因此线程的创建耗时并不是不可忽略的,只有当真的需要处理一个值得用线程去处理的任务时,才使用线程。一些鸡毛蒜皮的任务,就无需创建线程了。
据不精确估算创建一个线程大概需要0.24毫秒,随着线程的变多,这个值会变得更大,因此线程的创建耗时并不是不可忽略的,只有当真的需要处理一个值得用线程去处理的任务时,才使用线程。一些鸡毛蒜皮的任务,就无需创建线程了。
#### 创建多少线程合适
因为CPU的核心数限制当任务是密集型时就算线程数超过了CPU核心数也并不能帮你获得更好的性能因为每个线程的任务都可以轻松让CPU的某个核心跑满既然如此让线程数等于CPU核心数是最好的。
@ -220,7 +221,7 @@ fn main() {
但是当你的任务大部分时间都处于阻塞状态时就可以考虑增多线程数量这样当某个线程处于阻塞状态时会被切走进而运行其它的线程典型就是网络IO操作我们可以为每一个进来的用户连接创建一个线程去处理该连接绝大部分时间都是处于IO读取阻塞状态因此有限的CPU核心完全可以处理成百上千的用户连接线程但是事实上对于这种网络IO情况一般都不再使用多线程的方式了毕竟操作系统的线程数是有限的意味着并发数也很容易达到上限使用async/await的`M:N`并发模型,就没有这个烦恼。
#### 多线程的开销
下面的代码是一个lock-free的hashmap在多线程下的使用:
下面的代码是一个无锁实现的hashmap在多线程下的使用:
```rust
for i in 0..num_threads {
//clone the shared data structure
@ -243,15 +244,15 @@ for handle in handles {
}
```
按理来说,既然是`lock-free`了,那么锁的开销应该很小,性能会随着线程数的增加几近线程增长,但是真的是这样吗?
按理来说,既然是无锁实现了,那么锁的开销应该几乎没有,性能会随着线程数的增加几近线程增长,但是真的是这样吗?
下图是该代码在`48`核机器上的运行结果:
<img alt="" src="/img/threads-02.png" class="center" />
从图上可以明显的看出: 吞吐并不是线性增长,其从16核开始甚至开始肉眼可见的下降这是为什么呢
从图上可以明显的看出: 吞吐并不是线性增长,其从16核开始甚至开始肉眼可见的下降这是为什么呢
大概原因如下
限于书本的篇幅有限,我们只能给出大概原因:
- 虽然是无锁但是内部是CAS实现大量线程的同时访问会让CAS重试次数大幅增加
- 线程过多时CPU缓存的命中率会显著下降, 同时多个线程竞争一个CPU Cache-line的情况也会经常发生

Loading…
Cancel
Save