7.2 使用线程实现并发
在上一节中,我们了解了Rust的并发模型,其核心是“无畏并发”(Fearless Concurrency)——即通过类型系统和所有权机制在编译期消除数据竞争等常见并发错误。本节将深入实践,学习Rust中最基础的并发方式:使用操作系统原生线程。
7.2.1 创建线程:std::thread::spawn
Rust标准库通过std::thread模块提供了对操作系统线程的直接支持。创建新线程最简单的方式是使用thread::spawn函数,它接受一个闭包(closure)作为参数,该闭包包含了新线程要执行的代码。
use std::thread;
use std::time::Duration;
fn main() {
// 创建一个新线程
let handle = thread::spawn(|| {
for i in 1..10 {
println!("新线程: 数字 {}", i);
thread::sleep(Duration::from_millis(1));
}
});
// 主线程继续执行
for i in 1..5 {
println!("主线程: 数字 {}", i);
thread::sleep(Duration::from_millis(1));
}
// 等待新线程结束
handle.join().unwrap();
}
关键点:
thread::spawn返回一个JoinHandle类型。这个JoinHandle是一个拥有所有权的值,调用它的.join()方法会阻塞当前线程,直到对应的子线程执行完毕。- 如果不调用
join(),主线程可能会在子线程结束前退出,导致子线程被强制终止(或继续在后台运行,取决于操作系统)。 - 闭包中的代码将在新线程中独立执行。
7.2.2 线程与所有权:move闭包
由于thread::spawn中的闭包会在一个新线程中运行,Rust的所有权规则要求闭包必须拥有它捕获的所有数据的所有权。否则,主线程和子线程可能同时访问同一数据,导致数据竞争。
请看下面的错误示例:
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(|| {
// 错误!v的所有权属于main函数,闭包只能借用它
println!("向量: {:?}", v);
});
// 这里主线程也可能使用v,导致数据竞争
drop(v); // 主线程释放了v
handle.join().unwrap();
}
编译这段代码会得到错误,因为闭包试图借用v,但v的所有权在main函数中,而main函数可能在子线程执行期间释放v。
解决方案是使用move关键字,强制闭包获取它所用到的变量的所有权:
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(move || {
// v的所有权被转移到了闭包中
println!("向量: {:?}", v);
});
// 注意:此处不能再使用v,因为它的所有权已经被转移
// drop(v); // 这行代码会编译错误
handle.join().unwrap();
}
通过move闭包,v的所有权被安全地从主线程转移到了新线程。这保证了只有一个线程(新线程)可以访问v,从而避免了数据竞争。
7.2.3 线程的创建代价与限制
操作系统线程是重量级的资源。创建大量线程会带来显著的性能开销,包括:
- 内存开销:每个线程都有自己的栈(通常为几MB)。
- 上下文切换开销:操作系统调度大量线程会消耗CPU时间。
- 创建/销毁开销:线程的创建和销毁需要系统调用。
因此,对于I/O密集型或需要大量并发任务的场景,通常建议使用异步编程(如tokio)而非原生线程。但在CPU密集型任务或需要真正并行执行时,原生线程是合适的选择。
7.2.4 线程的调度与控制
除了spawn和join,std::thread模块还提供了其他控制线程的方法:
thread::sleep:让当前线程休眠指定的时间,常用于模拟耗时操作或让出CPU。thread::yield_now:提示操作系统调度器,当前线程愿意主动让出CPU时间片,给其他线程运行的机会。thread::current:获取当前线程的句柄。Builder:通过thread::Builder可以更精细地控制线程的创建,例如设置线程的名称和栈大小。
use std::thread;
let builder = thread::Builder::new()
.name("my-worker-thread".into())
.stack_size(32 * 1024); // 32KB栈
let handle = builder.spawn(|| {
println!("运行在自定义线程中");
}).unwrap();
handle.join().unwrap();
7.2.5 线程池
直接使用thread::spawn创建线程适合少量、长期运行的任务。对于大量短生命周期的任务,频繁创建和销毁线程的开销很大。此时,线程池(Thread Pool)是一种更优的解决方案。
Rust社区中流行的线程池库是rayon。它能够自动管理一个工作线程池,并将任务(通常是迭代器操作)并行化,极大地简化了数据并行编程。
use rayon::prelude::*;
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// 使用并行迭代器计算所有数字的平方和
let sum_of_squares: i32 = numbers.par_iter()
.map(|n| n * n)
.sum();
println!("平方和: {}", sum_of_squares);
}
7.2.6 小结
本节学习了Rust中使用std::thread进行并发编程的基础:
- 通过
thread::spawn创建线程,并通过JoinHandle::join等待其结束。 - 使用
move闭包将数据所有权安全地转移到新线程,避免数据竞争。 - 了解线程的创建代价、限制以及
Builder提供的细粒度控制。 - 认识到线程池(如
rayon)在处理大量短任务时的优势。
掌握这些基础后,下一节我们将探讨如何在线程之间安全地共享和修改数据,引入锁与互斥机制。
