rust线程

Rust 的线程模型基于底层操作系统的 1:1 线程模型,这意味着一个系统线程直接对应一个 Rust 线程。Rust 通过其独创的所有权(Ownership)和借用(Borrowing)机制,在编译期就能杜绝并发编程中最头疼的“数据竞争”问题。

1. 基础:创建线程与等待

在 Rust 中,使用标准库的 std::thread::spawn 可以轻松创建一个新线程。
use std::thread;
use std::time::Duration;

fn main() {
    // 创建一个新线程
    let handle = thread::spawn(|| {
        for i in 1..5 {
            println!("新线程: {}", i);
            thread::sleep(Duration::from_millis(1));
        }
    });

    for i in 1..3 {
        println!("主线程: {}", i);
        thread::sleep(Duration::from_millis(1));
    }

    // join() 会阻塞当前线程,直到 handle 对应的线程执行完毕
    handle.join().unwrap(); 
}
  • 主线程结束,程序即结束:如果主线程先执行完,新线程会被直接强行终止。
  • join() 方法:通过调用 handle.join(),主线程会等待子线程运行结束,确保任务完整执行。
1). 为什么 handle.join() 后面必须加 .unwrap()
在 Rust 中,handle.join() 返回的不是普通的 Result<T, E>,而是一个特殊的类型:
Result<T, Box<dyn Any + Send + 'static>> [1]
当子线程运行结束时,主线程通过 join() 会收到两种完全不同的执行结果。.unwrap() 的本质,是为了在子线程发生崩溃(Panic)时,让主线程也跟着一起崩溃,防止错误被无意中掩盖。
我们可以把这两类结果拆解来看:
    • Ok(T):子线程正常运行结束。.unwrap() 会直接解包并返回子线程闭包中的返回值 T [1]。
    • Err(Box<dyn Any + Send>):子线程在运行过程中发生了崩溃(Panic) [1]。Rust 的线程设计具有“Panic 隔离”特性:一个子线程挂了,默认不会导致整个进程挂掉。如果你不用 .unwrap() 去解包,主线程就会拿到这个 Err 并继续若无其事地往下走,这在大多数并发业务中是非常危险的。通过 .unwrap(),一旦子线程崩溃,主线程在解包时也会立即触发 Panic(即崩溃传递),确保系统能够及时暴露问题。
线程状态
1). Joinable(可连接状态)
在 Rust 中,通过 std::thread::spawn 创建的线程默认都是 Joinable 的。
特点
  • 生命周期受控:主线程可以(也通常应该)显式等待该线程结束。
  • 资源回收机制:当子线程执行完毕后,它的退出状态和部分底层资源会保留在系统中,直到有其他线程对其调用 .join(),这些资源才会被彻底清空。
  • 返回值传递:Joinable 线程允许将子线程中最后一行表达式的结果(或错误)返回给调用 .join() 的线程。
Rust 中的表现:JoinHandle
thread::spawn 会返回一个名为 JoinHandle<T> 的结构体。它就是操作该线程的“把手”。
2). Detached(分离状态)
分离状态意味着线程与创建它的父线程“断开”了联系。一旦线程被分离,它将完全脱离宿主线程的控制,由操作系统独立管理。
特点
  • 无法被等待:你不能对一个分离的线程调用 .join(),也无法得知它何时结束。
  • 无法获取返回值:子线程的计算结果或崩溃信息(Panic)无法直接传递回主线程。
  • 自动回收资源:当分离的线程执行完毕后,操作系统会自动立即回收它所占用的所有资源,不会留下任何痕迹。
  • 随主线程消亡:虽然它被称为“独立”,但如果主线程(main 进程)退出了,所有分离的子线程依然会被操作系统强制杀死。
Rust 中如何创建分离线程?
在 Rust 中,你不需要显式调用类似 detach() 的函数。Rust 巧妙地利用了所有权系统(Drop 机制)来实现分离:
规矩:如果你让 JoinHandle 离开了作用域(被销毁/Drop),且没有调用 .join(),那么这个线程在底层就会被隐式地转为分离(Detached)状态。

2. 线程安全的关键:move 闭包

当你想在子线程中使用主线程的变量时,普通的闭包会因为“借用检查”而报错(因为 Rust 无法确定主线程的变量能活得比子线程久)。此时必须使用 move 关键字。
use std::thread;

fn main() {
    let v = vec![1, 2, 3];

    // 使用 move 关键字将 v 的所有权强制转移(move)到新线程中
    let handle = thread::spawn(move || {
        println!("子线程使用 vector: {:?}", v);
    });

    // 此处不能再使用 v,因为所有权已经没了
    // println!("{:?}", v); // 编译报错!

    handle.join().unwrap();
}

3. 线程间通信:通道(Channel)

Rust 标准库提供了 mpsc(Multi-producer, Single-consumer,多生产者,单消费者)通道,专门用于线程间传递数据。
use std::sync::mpsc;
use std::thread;

fn main() {
    // 创建通道,tx 是发送端(Sender),rx 是接收端(Receiver)
    let (tx, rx) = mpsc::channel();

    // 克隆一个发送端,用于多个生产者
    let tx1 = tx.clone();

    // 生产者线程 1
    thread::spawn(move || {
        tx1.send(String::from("来自线程 1 的问候")).unwrap();
    });

    // 生产者线程 2
    thread::spawn(move || {
        tx.send(String::from("来自线程 2 的问候")).unwrap();
    });

    // 消费者(主线程)接收数据
    for received in rx {
        println!("收到: {}", received);
    }
}
  • send 方法会转移数据的所有权,防止发送后数据被修改引发混乱。
  • 当所有发送端(tx)都被释放时,迭代器 rx 会自动结束循环。

4. 共享内存并发:ArcMutex

如果你需要多个线程同时读写同一块内存数据,通道就不适用了。Rust 使用原子引用计数(Arc)和互斥锁(Mutex)来实现安全的共享内存。
use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    // Mutex 提供互斥访问,Arc 提供跨线程的多重所有权
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        let counter = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            // lock() 获取锁,如果锁被占用则等待。
            // lock_num 是一个智能指针(MutexGuard),离开作用域时自动解锁!
            let mut num = counter.lock().unwrap();
            *num += 1; 
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("最终计数结果: {}", *counter.lock().unwrap());
}

5. 线程安全底层:SendSync 特征

Rust 的并发安全并不是魔法,而是基于类型系统的两个内置特质(Trait):
  • Send:表明该类型的数据可以转移到其他线程(绝大多数类型都是 Send,但 Rc<T> 不是)。
  • Sync:表明该类型的数据可以被多个线程同时通过引用传递(如果 TSync,那么 &T 就是 Send)。
只要你在编写并发代码时违反了这两个特质,编译器就会直接拒绝编译,从而在源头上杜绝了运行时的并发 Bug。

💡 进阶建议

  • 不要过度使用原生线程:操作系统线程的创建和上下文切换是有开销的。
  • 计算密集型任务:推荐使用生态库 rayon 自动实现线程池和并行计算。
  • I/O 密集型任务(如网络请求):推荐使用 async/await 异步编程(如 tokio 运行时),而不是为每个请求开一个原生线程。
 

参考资料:

《Rust 权威指南》

posted @ 2026-07-23 17:25  PKICA  阅读(2)  评论(0)    收藏  举报