讲解

std::thread::spawn 创建操作系统级线程(1:1 模型),返回 JoinHandle——调用 .join() 等待线程结束并取回结果。最容易踩的坑是主线程先结束:不 join 的话,主线程退出时派生线程会被直接终止。闭包传给 spawn 时几乎总要带 move:线程可能活得比当前函数久,闭包必须拿走数据的所有权,借用局部变量编译器直接拒绝——「数据竞争在 Rust 里编译不过」就是从这些约束里长出来的。

线程间通信的 Rust 哲学是「不要通过共享内存来通信,要通过通信来共享内存」。std::sync::mpsc 通道(multiple producer, single consumer)是首选工具:channel() 返回 (tx, rx) 两端,tx.clone() 造出多个发送者,rx 逐个接收;send 会移动值的所有权,天然避免了「两边同时持有」的隐患。当所有发送端 drop 后,接收端的迭代器自然结束——忘了 drop 原始 tx 是接收端死等的经典原因。

确实需要共享可变状态时,工具是 Arc<Mutex>:Arc 是 Rc 的线程安全版(原子引用计数),Mutex 保证同一时刻只有一个线程能访问内部数据(lock() 拿到守卫,守卫离开作用域自动解锁)。类型系统在这里依然站岗:不把数据包进 Mutex 就想跨线程共享可变状态,编译器不给过——数据竞争从「线上偶发事故」降级为「写不出来」。

示例

spawn + join + move 闭包:

use std::thread;

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

    // move:线程拿走 data 的所有权(线程可能比 main 活得久)
    let handle = thread::spawn(move || {
        let sum: i32 = data.iter().sum();
        println!("子线程求和:{sum}");
        sum // join 可以取回这个返回值
    });

    // data 已移动,main 里不能再用
    let result = handle.join().expect("子线程 panicked");
    assert_eq!(result, 15);
    println!("主线程收到结果:{result}");
}

mpsc 通道:多生产者发消息,收集后排序保证确定性:

use std::sync::mpsc;
use std::thread;

fn main() {
    let (tx, rx) = mpsc::channel();
    let mut handles = vec![];

    for i in 0..3 {
        let sender = tx.clone(); // 每个线程一个发送端
        handles.push(thread::spawn(move || {
            sender.send(format!("消息 {i}")).unwrap();
        }));
    }
    drop(tx); // 关键:原始发送端 drop,接收迭代才会结束

    let mut received: Vec<String> = rx.iter().collect();
    for h in handles {
        h.join().unwrap();
    }

    received.sort(); // 到达顺序不定,排序后断言
    assert_eq!(received, vec!["消息 0", "消息 1", "消息 2"]);
    println!("共收到 {} 条消息", received.len());
}

共享状态:Arc<Mutex> 计数器:

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..5 {
        let counter = Arc::clone(&counter); // 克隆计数,共享同一份
        handles.push(thread::spawn(move || {
            *counter.lock().unwrap() += 1; // 锁守卫离开作用域自动解锁
        }));
    }

    for h in handles {
        h.join().unwrap();
    }
    assert_eq!(*counter.lock().unwrap(), 5);
    println!("五个线程各加一,结果正确");
}

常见坑

  • 忘记 join:主线程退出时派生线程被强制终止,任务可能跑一半;拿到 JoinHandle 就要想好谁来 join。
  • 接收端死等:发送端(包括原始 tx)没全部 drop,rx.iter() 会一直等下去;多生产者场景记得 drop 手里的 tx。
  • 以为共享可变状态能绕编译器:不包 Mutex 就跨线程改数据是编译错误——接受这个约束,它替你挡掉了最难排查的 bug 类别。
  • 锁粒度太大:一个全局 Mutex 包住所有状态,线程退化成串行;按数据拆分锁,或干脆回到消息传递。

小结

spawn/move/join 是线程三件套;mpsc 通道用通信代替共享;必须共享时 Arc<Mutex> 由类型系统护航。教程最后一章:让代码自带验证——自动化测试。