线程基础:std::thread详解
说到并发编程,线程是最基础的单位。Rust 的 std::thread 模块,说白了就是让你能轻松创建和管理操作系统线程。我个人习惯把它叫做「Rust 并发第一课」—— 你想想看,连线程都不会玩,后面那些高级并发工具根本用不上。
我刚开始用 Rust 写并发时,最直观的感受就是:这玩意儿比 C++ 安全太多了。不是说它不会出问题,而是编译器帮你拦住了大部分坑。嗯,咱们今天就把 std::thread 掰开揉碎了讲清楚。
线程创建与 Join
创建线程在 Rust 里非常简单。核心就是 thread::spawn 这个函数。它接收一个闭包,闭包里的代码会在新线程中执行。
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();
}
这里有个关键点:spawn 返回一个 JoinHandle。你调用 join() 时,主线程会等待子线程结束。我在项目中遇到过有人忘记调 join(),结果主线程跑完了直接退出,子线程还没执行完就被强制终止了。嗯,这个坑踩过一次就记住了。
join(),主线程退出时所有子线程都会被强制销毁。这不是优雅的关闭方式,数据可能丢失。
为什么会这样?因为 Rust 的线程是操作系统线程,主进程退出时,操作系统会回收所有资源。子线程还没来得及打印完,就被「咔嚓」了。
线程生命周期管理
线程的生命周期,说白了就是「从创建到结束」这段区间。但 Rust 的所有权系统让这件事变得有点微妙。
闭包捕获变量
当你把变量传入线程闭包时,所有权会被转移。看这个例子:
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(move || {
println!("向量: {:?}", v);
});
// 这里不能再使用 v,因为所有权已经转移了
// println!("{:?}", v); // 编译错误!
handle.join().unwrap();
}
注意那个 move 关键字。它告诉闭包:把捕获的变量所有权拿过来。我曾经在项目里忘记加 move,结果编译器报了一堆生命周期错误。你想想看,如果不转移所有权,主线程和子线程同时访问同一个变量,那不就成数据竞争了吗?Rust 编译器可不会让你这么干。
move 硬搬。用 Arc<Mutex<T>> 或者 Arc<RwLock<T>> 才是正道。后面章节会详细讲。
线程的三种状态
每个线程都有生命周期状态。我习惯把它分成三个阶段:
| 状态 | 说明 | 常见问题 |
|---|---|---|
| 创建期 | 调用 spawn 后,线程被调度执行 |
闭包捕获变量时所有权问题 |
| 运行期 | 线程正在执行闭包中的代码 | panic 会导致线程崩溃,但不影响其他线程 |
| 结束期 | 线程执行完毕,等待 join 回收 |
忘记 join 导致线程被强制终止 |
这里有个细节:子线程 panic 不会传播到主线程。主线程调用 join() 时会返回 Result,你可以检查子线程是否 panic 了。
use std::thread;
fn main() {
let handle = thread::spawn(|| {
panic!("子线程炸了!");
});
let result = handle.join();
match result {
Ok(_) => println!("子线程正常结束"),
Err(e) => println!("子线程出错了: {:?}", e),
}
}
我个人习惯在生产代码里总是检查 join() 的返回值。你想想看,如果子线程偷偷 panic 了,主线程还傻傻地以为一切正常,那 bug 就藏得深了。
线程的命名与标识
调试多线程程序时,给线程起个名字能省不少事。Rust 提供了 Builder 模式来创建带名字的线程:
use std::thread;
fn main() {
let builder = thread::Builder::new()
.name("数据采集线程".into())
.stack_size(1024 * 1024); // 1MB 栈空间
let handle = builder.spawn(|| {
println!("我在线程: {:?}", thread::current().name());
}).unwrap();
handle.join().unwrap();
}
我在项目中遇到过一个问题:默认线程栈大小是 2MB,但某个数据处理任务需要递归调用,栈不够用了。用 Builder 调整栈大小后问题解决。嗯,这种问题在日志里很难排查,所以提前命名线程、设置合适的栈大小,都是好习惯。
thread::spawn创建线程,返回JoinHandlejoin()等待线程结束,必须调用move关键字转移变量所有权到闭包- 子线程 panic 不会影响主线程,但需要检查
join()结果 - 用
Builder设置线程名和栈大小,方便调试
知识体系图
下面这张图展示了本章的核心逻辑。从线程创建到生命周期管理,每一步都有对应的工具和注意事项。
这张图把本章的知识点串起来了。从 spawn 创建线程开始,到 join 回收结束,中间涉及闭包捕获、错误处理、Builder 配置。你想想看,每一步都有 Rust 的所有权系统在背后保驾护航。
好了,线程基础就讲到这里。记住一句话:创建线程容易,管理生命周期难。我在项目中见过太多因为忘记 join 或者忽略 move 导致的 bug。嗯,这些坑你踩过一次就再也不会忘了。