5分钟速读之Rust权威指南(三十)多线程多线程

多线程

前端同学对于WebWorker肯定比较熟悉,对于计算量大的业务,我们可以将计算逻辑分配到多个线程去处理,减少主线程的压力,提高处理速度,在rust中启用多线程很方便,如果用JS的话来说,启动一个线程就像传递一个回调函数一样简单

使用 spawn 创建新线程

使用标准库中的thread模块创建一个spawn子线程:

use std::thread; // 引入thread
use std::time::Duration; // 引入time::Duration,用来创建时间类型数据

thread::spawn(|| {
  for i in 1..10 {
    println!("spawn: {}", i);
    // Duration::from_secs方法创建单位为秒的时间类型数据
    // 使用sleep方法让spawn线程停止一秒
    thread::sleep(Duration::from_secs(1));
  }
});

for i in 1..5 {
  println!("main: {}", i);
  // 让主线程线程停止一秒
  thread::sleep(Duration::from_secs(1));
}
// main: 1
// spawn: 1
// main: 2
// spawn: 2
// main: 3
// spawn: 3
// main: 4
// spawn: 4
// spawn: 5
复制代码

当主线程结束时,spawn线程也会结束,而不管其是否执行完毕,上面spawn线程并没有执行完成。

使用 join 等待所有线程结束

我们可以使用thread::spawn返回值的join方法控制让main线程去等待spawn线程的执行完成:

// 获取spawn线程管理工具
let handle = thread::spawn(|| {
  for i in 1..10 {
    println!("spawn: {}", i);
    thread::sleep(Duration::from_secs(1));
  }
});

for i in 1..5 {
  println!("main: {}", i);
  thread::sleep(Duration::from_secs(1));
}
// 阻塞主线程,等待spawn线程执行
handle.join().unwrap();
// main: 1
// spawn: 1
// main: 2
// spawn: 2
// main: 3
// spawn: 3
// spawn: 4
// main: 4
// spawn: 5
// spawn: 6
// spawn: 7
// spawn: 8
// spawn: 9
复制代码

join会阻塞当前线程直到handle线程结束,阻塞(Blocking)线程意味着阻止该线程执行工作或退出。


如果将handle.join()移动到for循环之前呢?可以猜一下是怎么输出的:

// 略...
handle.join().unwrap();
for i in 1..5 {
  println!("main: {}", i);
  thread::sleep(Duration::from_secs(1));
}
复制代码

输出结果:

// spawn: 1
// spawn: 2
// spawn: 3
// spawn: 4
// spawn: 5
// spawn: 6
// spawn: 7
// spawn: 8
// spawn: 9
// main: 1
// main: 2
// main: 3
// main: 4
复制代码

可以看到spawn线程的循环会先执行完成,再执行主线程的循环。

线程与 move 闭包

上面的spawn线程中的代码对main线程中的数据没有引用,当spawn线程使用main线程中的数据时:

let n = vec![1,2,3];
	
let handle = thread::spawn(|| { // 报错,闭包可能比当前函数存活的时间长,但闭包它借用了'n',而'n'是当前函数拥有的
  println!("来自main线程的数据: {:?}", n);
});

handle.join().unwrap();
复制代码

上边闭包尝试借用 v。然而这有一个问题:rust 不知道这个新建线程会执行多久,所以无法知晓 v 的引用是否一直有效,例如:

let n = vec![1,2,3];
	
let handle = thread::spawn(|| {
	println!("来自main线程的数据: {:?}", n);
});

drop(n); // 销毁n
handle.join().unwrap();
复制代码

因为当线程中对n有借用,在线程还没执行的时候,后边的drop已经将n丢弃了。


我们可以通过在闭包之前增加 move 关键字,强制闭包获取其使用的n的所有权:

let n = vec![1, 2, 3];
	
let handle = thread::spawn(move || {
  println!("来自main线程的数据: {:?}", n); // 来自main线程的数据: [1, 2, 3]
});

handle.join().unwrap();
复制代码

上面代码中,将n的所有权移动到了spawn闭包中,所以能够正常执行。


如果仍然使用drop的话:

let n = vec![1, 2, 3];

let handle = thread::spawn(move || {
  println!("来自main线程的数据: {:?}", n);
});

drop(n); // 报错,n已经被移动到上边的闭包中,不能再次在这里使用
handle.join().unwrap();
复制代码

因为n已经被移动到了spawn闭包中,所以不能在后面以任何方式继续使用,即使println!("{:?}",n),也是不允许的。