|
|
|
@ -216,9 +216,9 @@ fn main() {
|
|
|
|
|
据不精确估算,创建一个线程大概需要0.24毫秒,随着线程的变多,这个值会变得更大,因此线程的创建耗时并不是不可忽略的,只有当真的需要处理一个值得用线程去处理的任务时,才使用线程。一些鸡毛蒜皮的任务,就无需创建线程了。
|
|
|
|
|
|
|
|
|
|
#### 创建多少线程合适
|
|
|
|
|
因为CPU的核心数限制,当任务是密集型时,就算线程数超过了CPU核心数,也并不能帮你获得更好的性能,因为每个线程的任务都可以轻松让CPU的某个核心跑满,既然如此,让线程数等于CPU核心数是最好的。
|
|
|
|
|
因为CPU的核心数限制,当任务是CPU密集型时,就算线程数超过了CPU核心数,也并不能帮你获得更好的性能,因为每个线程的任务都可以轻松让CPU的某个核心跑满,既然如此,让线程数等于CPU核心数是最好的。
|
|
|
|
|
|
|
|
|
|
但是当你的任务大部分时间都处于阻塞状态时,就可以考虑增多线程数量,这样当某个线程处于阻塞状态时,会被切走,进而运行其它的线程,典型就是网络IO操作,我们可以为每一个进来的用户连接创建一个线程去处理,该连接绝大部分时间都是处于IO读取阻塞状态,因此有限的CPU核心完全可以处理成百上千的用户连接线程,但是事实上,对于这种网络IO情况,一般都不再使用多线程的方式了,毕竟操作系统的线程数是有限的,意味着并发数也很容易达到上限,使用async/await的`M:N`并发模型,就没有这个烦恼。
|
|
|
|
|
但是当你的任务大部分时间都处于阻塞状态时,就可以考虑增多线程数量,这样当某个线程处于阻塞状态时,会被切走,进而运行其它的线程,典型就是网络IO操作,我们可以为每一个进来的用户连接创建一个线程去处理,该连接绝大部分时间都是处于IO读取阻塞状态,因此有限的CPU核心完全可以处理成百上千的用户连接线程,但是事实上,对于这种网络IO情况,一般都不再使用多线程的方式了,毕竟操作系统的线程数是有限的,意味着并发数也很容易达到上限,而且过多的线程也会导致线程上下文切换的代价过大,使用async/await的`M:N`并发模型,就没有这个烦恼。
|
|
|
|
|
|
|
|
|
|
#### 多线程的开销
|
|
|
|
|
下面的代码是一个无锁实现(CAS)的hashmap在多线程下的使用:
|
|
|
|
@ -333,7 +333,7 @@ FOO.with(|f| {
|
|
|
|
|
});
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
上面代码中,`FOO`即是我们创建的**线程局部变量**,每个新的线程访问它时,都会使用它的初始值作为开始,各个线程中的`FOO`值彼此互不干扰。
|
|
|
|
|
上面代码中,`FOO`即是我们创建的**线程局部变量**,每个新的线程访问它时,都会使用它的初始值作为开始,各个线程中的`FOO`值彼此互不干扰。注意`FOO`使用`static`声明为生命周期为`'static`的静态变量。
|
|
|
|
|
|
|
|
|
|
可以注意到,线程中对`FOO`的使用是通过借用的方式,但是若我们需要每个线程独自获取它的拷贝,最后进行汇总,就有些强人所难了。
|
|
|
|
|
|
|
|
|
@ -434,8 +434,8 @@ fn main() {
|
|
|
|
|
|
|
|
|
|
上述代码流程如下:
|
|
|
|
|
|
|
|
|
|
1. `main`线程首先进入`while`循环,并释放了锁`started`,然后开始挂起等待子线程的通知
|
|
|
|
|
2. 子线程获取到锁,并将其修改为true, 然后调用条件的方法来通知主线程继续执行:`cvar.notify_one`
|
|
|
|
|
1. `main`线程首先进入`while`循环,调用`wait`方法挂起等待子线程的通知,并释放了锁`started`
|
|
|
|
|
2. 子线程获取到锁,并将其修改为true, 然后调用条件变量的`notify_one`方法来通知主线程继续执行
|
|
|
|
|
|
|
|
|
|
## 只被调用一次的函数
|
|
|
|
|
有时,我们会需要某个函数在多线程环境下只被调用一次,例如初始化全局变量,无论是哪个线程先调用函数来初始化,都会保证全局变量只会被初始化一次,随后的其它线程调用就会忽略该函数:
|
|
|
|
@ -472,6 +472,14 @@ fn main() {
|
|
|
|
|
|
|
|
|
|
代码运行的结果取决于哪个线程先调用`INIT.call_once`(虽然代码具有先后顺序,但是线程的初始化顺序并无法被保证!因为线程初始化是异步的,且耗时较久),若`handle1`先,则输出`1`,否则输出`2`。
|
|
|
|
|
|
|
|
|
|
**call_once 方法**
|
|
|
|
|
|
|
|
|
|
执行初始化过程一次,并且只执行一次。
|
|
|
|
|
|
|
|
|
|
如果当前有另一个初始化过程正在运行,该方法将阻止调用的线程。
|
|
|
|
|
|
|
|
|
|
当这个函数返回时,保证一些初始化已经运行并完成,它还保证由执行的闭包所执行的任何内存写入都能被其他线程在这时可靠地观察到。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## 总结
|
|
|
|
|
[Rust的线程模型](./intro.md)是`1:1`模型,因为Rust要保持尽量小的运行时。
|
|
|
|
|