本文接上一篇rust学习记录,在对闭包进行学习后开始学习Rust中线程的使用,下文即对笔者在学习中所学到的知识及遇到的问题和思考进行记录总结.
该篇文章同样参考自视频b站令狐一冲Rust进阶
首先在这里先介绍进程:
进程是资源分配的最小单位,而线程是CPU调度的最小单位。也就是说一个进程下会有多个线程同时存在,这些线程共同使用一套映射表。
而在使用多线程时,经常会遇到的一些问题:
(1)竞争状态:多个线程以不一致的顺序访问数据或资源;
(2)死锁:两个线程相互等待对方停止使用其所拥有的资源,造成两者都永久等待,示例如下:
A: 1->2->3 B: 2->1->3
t1时刻: A:1 B:2
接下来等待 :A:2 B:1
(3)只会发生在特定情况下且难以稳定重现和修复的bug
编程语言提供的线程叫做绿色线程,如go语言,在底层实现了M:N的模型,即M个绿色线程对应N个OS线程。但是Rust标准库只提供1:1的线程模型的实现,,即>一个Rust线程对应一个OS线程,不过Rust中也有实现了M:N的模型结构。
代码如下:
use std::thread;
use std::time::Duration;
fn main() {
thread::spawn(||{
//创建一个子线程
for i in 1..10 {
println!("number {} in spawn thread!",i);
thread::sleep(Duration::from_millis(1));
//规定输出后的睡眠时间
}
});
for j in 1..5 {
//这里是主线程的任务
println!("number {} in main thread!",j);
thread::sleep(Duration::from_millis(1));
}
println!("main thread end!");
}
如上代码就实现了在同一个进程下拥有两个线程(main线程和spawn线程),但是经过编译运行后发现结果有些奇怪,如下所示:
root@ThinkPad-T540p:~/learn_rust/learn_thread# cargo run
Compiling learn_thread v0.1.0 (/root/learn_rust/learn_thread)
Finished dev [unoptimized + debuginfo] target(s) in 1.45s
Running `target/debug/learn_thread`
-----------------------------------------
number 1 in main thread!
number 1 in spawn thread!
number 2 in main thread!
number 2 in spawn thread!
number 3 in main thread!
number 3 in spawn thread!
number 4 in main thread!
number 4 in spawn thread!
main thread end!
在仔细观察结果后发现,main线程中的for循环是执行完毕的,但是在main线程结束后,spawn线程即子线程的for循环被强制结束了,这是实际环境中不可被接受的。
于是针对该问题对代码进行了改进,如下所示:
use std::thread;
use std::time::Duration;
fn main() {
let handle = thread::spawn(||{
//创建一个子线程
for i in 1..10 {
println!("number {} in spawn thread!",i);
thread::sleep(Duration::from_millis(1));
//规定输出后的睡眠时间
}
});
for j in 1..5 {
//这里是主线程的任务
println!("number {} in main thread!",j);
thread::sleep(Duration::from_millis(1));
}
println!("main thread end!");
handle.join().unwrap();
//通过join()让主线程结束后等待子线程结束
//如果不设置,则主线程结束后自动中断子线程
}
如上,在代码段末尾添加了join方法,作用见注释
编译结果:
root@ThinkPad-T540p:~/learn_rust/learn_thread# cargo run
Compiling learn_thread v0.1.0 (/root/learn_rust/learn_thread)
Finished dev [unoptimized + debuginfo] target(s) in 0.47s
Running `target/debug/learn_thread`
-----------------------------------------
number 1 in main thread!
number 1 in spawn thread!
number 2 in main thread!
number 2 in spawn thread!
number 3 in main thread!
number 3 in spawn thread!
number 4 in spawn thread!
number 4 in main thread!
main thread end!
number 5 in spawn thread!
number 6 in spawn thread!
number 7 in spawn thread!
number 8 in spawn thread!
number 9 in spawn thread!
可见子线程被中断的问题已经解决,同时如果将join()方法放在子线程和主线程中间再进行编译运行,则会使两线程隔离相继运行,代码及编译结果如下:
代码:
use std::thread;
use std::time::Duration;
fn main() {
let handle = thread::spawn(||{
//创建一个子线程
for i in 1..10 {
println!("number {} in spawn thread!",i);
thread::sleep(Duration::from_millis(1));
//规定输出后的睡眠时间
}
});
handle.join().unwrap();
//如果将join()设置在这里,则会先进行子线程,等待子线程结束后才会进入主线程执行任务
println!("-----------------------------------------");
for j in 1..5 {
//这里是主线程的任务
println!("number {} in main thread!",j);
thread::sleep(Duration::from_millis(1));
}
println!("main thread end!");
//handle.join().unwrap();
//通过join()让主线程结束后等待子线程结束
//如果不设置,则主线程结束后自动中断子线程
}
编译结果:
root@ThinkPad-T540p:~/learn_rust/learn_thread# cargo run
Compiling learn_thread v0.1.0 (/root/learn_rust/learn_thread)
Finished dev [unoptimized + debuginfo] target(s) in 0.43s
Running `target/debug/learn_thread`
number 1 in spawn thread!
number 2 in spawn thread!
number 3 in spawn thread!
number 4 in spawn thread!
number 5 in spawn thread!
number 6 in spawn thread!
number 7 in spawn thread!
number 8 in spawn thread!
number 9 in spawn thread!
-----------------------------------------
number 1 in main thread!
number 2 in main thread!
number 3 in main thread!
number 4 in main thread!
main thread end!
在上文中,笔者根据视频,对多线程的初步简单使用做了一些应用,接下来对于更加具体的问题(闭包中的悬垂引用)作如下探讨:
可以发现,线程的使用依赖于闭包结构,当我们将无copy trait的变量加入到线程应用中时,会发现出现了一些意料之外情理之中的报错,代码以及编译结果如下:
代码:
use std::thread;
use std::time::Duration;
fn main() {
let v = vec![1,2,3];
let handle = thread::spawn(||{
println!("{:?}",v);
});
handle.join().unwrap();
println!("Hello, world!");
}
编译结果:
root@peryol-ThinkPad-T540p:~/learn_rust/learn_thread2# cargo run
Compiling learn_thread2 v0.1.0 (/root/learn_rust/learn_thread2)
warning: unused import: `std::time::Duration`
--> src/main.rs:2:5
|
2 | use std::time::Duration;
| ^^^^^^^^^^^^^^^^^^^
|
= note: `#[warn(unused_imports)]` on by default
error[E0373]: closure may outlive the current function, but it borrows `v`, which is owned by the current function
--> src/main.rs:7:32
|
7 | let handle = thread::spawn(||{
| ^^ may outlive borrowed value `v`
10 | println!("{:?}",v);
| - `v` is borrowed here
|
note: function requires argument type to outlive `'static`
--> src/main.rs:7:18
|
7 | let handle = thread::spawn(||{
| __________________^
8 | | ...
9 | |
10 | | println!("{:?}",v);
11 | | });
| |______^
help: to force the closure to take ownership of `v` (and any other referenced variables), use the `move` keyword
|
7 | let handle = thread::spawn(move ||{
| ^^^^^^^
For more information about this error, try `rustc --explain E0373`.
warning: `learn_thread2` (bin "learn_thread2") generated 1 warning
error: could not compile `learn_thread2` due to previous error; 1 warning emitted
如上述示例,此处利用闭包捕获了作用域中的vec变量,但是由于不确定v的引用是否一直有效,如不额外声明move,则会发生报错may outlive borrowed value v
举例:当进入子线程后,假如要先睡眠10秒,而此时主线程中将v进行提前drop,就会造成闭包中出现悬垂引用,导致编译报错。
所以对代码进行改进,加入move特性,即
use std::thread;
//use std::time::Duration;
fn main() {
let v = vec![1,2,3];
let handle = thread::spawn(move ||{
println!("{:?}",v);
});
handle.join().unwrap();
println!("Hello, world!");
}
root@ThinkPad-T540p:~/learn_rust/learn_thread2# cargo run
Compiling learn_thread2 v0.1.0 (/root/learn_rust/learn_thread2)
Finished dev [unoptimized + debuginfo] target(s) in 0.30s
Running `target/debug/learn_thread2`
[1, 2, 3]
Hello, world!
在解决问题的同时需要注意,这里闭包使用了move,则将v的所有权转移给了闭包,此时是无法在main作用域中再次使用v的。
综上,笔者通过跟随视频学习与上述的总结,对Rust的多线程使用有了一定的了解,并针对具体问题进行了实验与探讨,对于文中所写,如果各位读者有何见解或不同意见,还请您不吝赐教。