16 无畏并发
安全高效的处理并发编程是Rust的另一个主要的目标
内存安全和高效编程一直都是很多语言追求的目的,Rust采用所有权和类型系统来平衡处理这一点
本章我们将会了解
1.如何创建线程来同时运行多端代码
2.消息传递并发,其中channel 被用来在线程之间传递消息
3.共享状态并发,其中多个线程可以访问同一片数据
4.Sync和Send trait,将Rust的并发保证扩展到用户定义的以及标准库提供的类型中
16.1 使用线程同时运行代码
在大部分的现代操作系统中,已执行的程序代码都在一个进程中,进程由操作系统来管理,但是在进程的内部,也可以拥有多个同时运行的独立部分,它们被称为线程
将程序拆分为多个线程虽然能够提高运行效率,但是会增加复杂性,如:
1.竞争状态:多个线程以不一致的顺序访问数据或者资源
2.死锁:两个线程相互等待对方停止使用其所拥有的资源,从而组织程序继续运行
3.只会发生在特定情况且难以稳定重现和修复bug
Rust尝试减轻使用线程的负面影响
编程语言有一些不同的方法来实现线程。很多操作系统提供了创建新线程的API,然后编程语言可以调用,这种模型被称为1:1,一个OS线程对应一个语言线程
编程语言提供的线程被称为绿色线程,使用绿色线程的语言会在不同数量的OS线程的上下文中执行它们。这种模型被称为M:N模型,M个绿色线程对应N个OS线程,M不等于N
对于Rust来说取舍是运行时支持
此刻的运行时指的是二进制文件中包含的由语言自身提供发的代码,此类代码根据语言的不同可大可小,不过任何非汇编语言都会有一定数量的运行时代码。Rust几乎没有运行时
绿色线程的M:N模型需要更大的语言运行时来管理这些线程。因此,Rust标准库只提供了1:1线程模型实现。由于Rust是较为底层的语言,如果你愿意牺牲性能来换取抽象,以获得对线程运行更精细的控制及更低的上下文切换成本,你可以使用实现了M:N线程模型的crate
现在让我们使用标准库提供的相关的API
使用spawn创建新线程
为了创建新线程,我们需要调用thread::Duration函数并传递给一个闭包,并在其中包含希望在新线程中运行的代码
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));
}
}
运行结果大概像如下这样,并且当主线程结束后,新线程也会结束,而不管其是否执行完毕
Running `target/debug/smartPoint`
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 main thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!
thread::sleep调用强制程序停止执行一小段时间,这里允许其他不同的线程运行,它们可能会轮流运行,但是也不总是如此
使用join等待所有线程结束
我们现在来修复新线程没有执行完的问题
我们通过将thread::spawn的返回值存储在变量中来修复这个问题,它的返回值类型是JoinHandle,JoinHandle是一个拥有所有权的值,当对其调用join方法时,它会等待其线程结束
fn main(){
let handle =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));
}
handle.join().unwrap();
}
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 main thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!
hi number 6 from the spawned thread!
hi number 7 from the spawned thread!
hi number 8 from the spawned thread!
hi number 9 from the spawned thread!
通过结果我们看到前面部分两个线程仍然会交替进行,但是由于handle.join,程序会当所有线程执行完才退出,让我们看看到底发生了什么
fn main(){
let handle =thread::spawn(||{
for i in 1..10{
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));
}
}
Running `target/debug/smartPoint`
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 5 from the spawned thread!
hi number 6 from the spawned thread!
hi number 7 from the spawned thread!
hi number 8 from the spawned thread!
hi number 9 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会强制让所指的线程执行完,但是它放在哪里是会影响程序性能的
线程与move闭包
move闭包允许我们在一个线程中使用另一个线程的数据,常和thread::spawn一起使用
在参数列表前使用move关键字可以强制闭包获取其使用的环境值的所有权。这个技巧在创建新线程将值的所有权从一个线程移动到另一个线程时最为实用
上面的thread::spawn的闭包没有任何参数:并没有在新建线程代码中使用任何主线程的数据,下面我们来尝试一下
fn main(){
let v = vec![1,2,3];
let handle = thread::spawn(||{
println!("Here's a vector: {:?}",v);
});
handle.join().unwrap();
}
闭包使用了v,所以闭包会捕获v并使其成为闭包环境的一部分,因为新线程在运行这个闭包,所以可以在新线程中访问v,但是我们会遇到错误
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,因为println!只要用v的引用,闭包尝试借用v.然而这是一个问题:Rust并不知道这个新建线程会执行多久,所以无法知晓v的引用是否一直有效
fn main(){
let v = vec![1,2,3];
let handle = thread::spawn(||{
println!("Here's a vector: {:?}",v);
});
drop(v);
handle.join().unwrap();
}
我们再来看看这个例子,主线程一下子就把v丢弃了,所以新线程开始执行时v已不再有效
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();
}
现在我们来采取错误信息的建议在闭包之前添加关键字move,强制闭包获取其使用值的所有权,而不是任由Rust推断他应该借用值。上面的代码可以正常输出结果
Running `target/debug/smartPoint`
Here's a vector: [1, 2, 3]
但是如果我们加上drop呢
use std::thread;
fn main(){
let v = vec![1,2,3];
let handle = thread::spawn(move ||{
println!("Here's a vector: {:?}",v);
});
drop(v);
handle.join().unwrap();
}
error[E0382]: use of moved value: `v`
--> src/main.rs:8:10
|
3 | let v = vec![1,2,3];
| - move occurs because `v` has type `Vec<i32>`, which does not implement the `Copy` trait
4 |
5 | let handle = thread::spawn(move ||{
| ------- value moved into closure here
6 | println!("Here's a vector: {:?}",v);
| - variable moved due to use in closure
7 | });
8 | drop(v);
| ^ value used here after move
我们看到既然已经move了,就不能再drop了。这就是Rust所有权规则再次帮助了我们,move意味着我们不会在主线程中使用v了