0x00 开篇
到目前为止,我们所写的程序都是单线程运行的,然而在实际开发的工作环境中,我们经常会遇见多任务处理,比如一个下载任务。当下载任务开启时,我们还需要处理其它事情,这时单线程的任务并不满足我们的需求。从本篇文章开始,我们将开启并发编程的学习。
0x01 线程的创建与使用
创建线程通常被称为 产生线程
(spawning thread)。我们使用 std::thread::spawn
函数,该函数接收一个 FnOnce
型的闭包。源码如下:
// std::thread::spawn
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
F: FnOnce() -> T,
F: Send + 'static,
T: Send + 'static,
{
Builder::new().spawn(f).expect("failed to spawn thread")
}
产生线程的时候,这些线程被认为是从父线程复刻(fork)出来的,这也就是我们常说的 fork 一个线程
。来看下示例代码:
fn main() {
let duration = Duration::from_millis(3000);
thread::spawn(move || {
thread::sleep(duration);
println!("线程1 执行完成");
});
thread::spawn(move || {
thread::sleep(duration);
println!("线程2 执行完成");
});
println!("程序 执行结束");
}
// 运行结果
// 程序 执行结束
上面的代码是创建了两个线程,在每个线程里面 休息3秒钟
后输出 线程x 执行完成
,最后再输出 程序 执行结束
。运行后发现,程序仅打印了 程序 执行结束
。这是由于,我们在创建线程之后,父线程的程序继续执行,并没有去“理会”子线程的运行结果。当父线程都已经执行结束了,子线程还没有执行或者执行到一半。在实际操作中,我们还需要将线程连接起来。我们使用 join
将线程捆绑合并在一起。join
函数的作用就是等待另一个线程执行结束。修改后的代码如下:
fn main() {
let duration = Duration::from_millis(3000);
let handle1 = thread::spawn(move || {
thread::sleep(duration);
println!("线程1 执行完成");
});
let handle2 = thread::spawn(move || {
thread::sleep(duration);
println!("线程2 执行完成");
});
// 等待线程结束
handle1.join().unwrap();
handle2.join().unwrap();
println!("程序 执行结束");
}
// 运行结果
// 线程1 执行完成
// 线程2 执行完成
// 程序 执行结束
spawn 为什么需要传入 FnOnce 闭包
我们产生的子线程可能会比父线程 活
的更久,这就意味着需要通过 move
来将其所有权转到子线程中。
0x02 跨线程共享“不可变数据”
通常情况下,我们会存在多线程使用同一个数据的场景。假设我有两个线程都需要使用一组数据,大家最先想到的就是 clone
一份数据。当然,这样做是可以解决问题的。假设这组数据非常大呢?有 10 个线程要共享呢?这就需要我前面《Rust 中级教程 第12课》所介绍的 Arc
——原子引用计数。示例代码如下:
fn main() {
let duration = Duration::from_millis(1000);
// 这样可以解决问题,但在数据量很大的时候,不推荐
// let data = vec!["hello".to_string(), "rust".to_string()];
// Arc 原子引用计数
let data: Arc<Vec<String>> = Arc::new(vec!["hello".to_string(), "rust".to_string()]);
// clone 仅会增加引用计数,不会真正的 clone 数据
let handle1_data = data.clone();
let handle1 = thread::spawn(move || {
thread::sleep(duration);
println!("线程1 {:?}", handle1_data);
println!("线程1 执行完成");
});
// clone 仅会增加引用计数,不会真正的 clone 数据
let handle2_data = data.clone();
let handle2 = thread::spawn(move || {
thread::sleep(duration);
println!("线程2 {:?}", handle2_data);
println!("线程2 执行完成");
});
// 等待线程结束
handle1.join().unwrap();
handle2.join().unwrap();
println!("程序 执行结束");
}
// 程序运行结果
// 线程2 ["hello", "rust"]
// 线程2 执行完成
// 线程1 ["hello", "rust"]
// 线程1 执行完成
// 程序 执行结束
我们调用 data.clone
方法时,并不会再次创建一个 data
副本,而是增加了一次引用计数。只要有任何线程拥有 Arc<Vec<String>>
,映射就不会释放,即使是父线程已经不存在了。由于 Arc
中的数据是不可变的,所以不会出现数据竞争的问题。
0x03 JoinHandle
std::thread::spawn
的返回值是 JoinHandle
, 它是一个结构体,包含一个 JoinInner
类型。源码如下:
pub struct JoinHandle<T>(JoinInner<'static, T>);
/// Inner representation for JoinHandle
struct JoinInner<'scope, T> {
native: imp::Thread,
thread: Thread,
packet: Arc<Packet<'scope, T>>,
}
JoinHandle
用于等待线程完成其执行并检索它所产生的结果。它里面有几个重要的方法简单介绍下:
join
要等待线程完成并获取其结果,可以调用 JoinHandle
的 join
方法(上面已经介绍过了)。该方法会阻塞当前线程,直到与 JoinHandle
关联的线程完成其执行,然后返回线程产生的结果。如果线程发生错误(panic),则 join
方法也会抛出错误(panic)。
另外子线程的返回结果也会通过 join
方法返回。示例代码如下:
fn main() {
let duration = Duration::from_millis(1000);
let handle = thread::spawn(move || {
thread::sleep(duration);
// 返回一个 String 类型
return "hello".to_string();
});
// 父线程接收子线程运行结束的结果
let result = handle.join().unwrap();
println!("{}", result);
}
// 运行结果
// hello
is_finished
该方法从 Rust 1.61.0
版本变为 stable
。该方法用于检查与 JoinHandle
相关联的线程是否已经执行完成。通常情况下,在调用 join
方法之前,我们可以使用 is_finished
方法来查询与 JoinHandle
对象相关联的线程是否已经完成执行。如果返回值为 true
,则说明线程已经执行完成,并且可以通过调用 join
方法来等待线程退出并获取其返回值。示例代码如下:
fn main() {
let duration = Duration::from_millis(1000);
let handle = thread::spawn(move || {
thread::sleep(duration);
// 返回一个 String 类型
return "hello".to_string();
});
let handle_finished = handle.is_finished();
println!("handle_finished: {}", handle_finished);
// 父线程接收子线程运行结束的结果
let result = handle.join().unwrap();
println!("{}", result);
}
// 运行结果
// handle_finished: false
// hello
thread
我们可以使用 thread
方法从 JoinHandle
中获取线程的一些信息,像线程 Id
, 线程的名称等。示例代码如下:
fn main() {
let duration = Duration::from_millis(1000);
let handle = thread::spawn(move || {
thread::sleep(duration);
// 返回一个 String 类型
return "hello".to_string();
});
let thread = handle.thread();
println!("{:?}", thread);
// 父线程接收子线程运行结束的结果
let result = handle.join().unwrap();
println!("{}", result);
}
// 运行结果
// Thread { id: ThreadId(2), name: None, .. }
// hello
0x04 小结
读完文章感觉怎么样呢?有没有感觉 Rust 创建一个线程很简单呢,哈哈哈。到目前为止,你基本已经对 Rust 的并发编程有初步了解了。我们将在下一篇文章继续更深入的了解并发编程。