楼层: 首页/ 软件技术/ Rust 语言基础/ 无畏并发:线程、消息、锁
13

无畏并发:线程、消息、锁

Fearless Concurrency · Threads · Channels · Mutex

并发是 Rust 的杀手锏之一。别的语言靠程序员别写数据竞争,Rust 靠 Send/Sync 两个 trait 在编译期保证——代码能编过,就大概率不会数据竞争。这就叫"无畏并发"。

线程:std::thread::spawn

use std::thread; use std::time::Duration; fn main() { // spawn 起一个线程;move 把环境所有权搬进线程 let handle = thread::spawn(move || { for i in 0..5 { println!("子线程:{}", i); thread::sleep(Duration::from_millis(10)); } }); handle.join().unwrap(); // 等子线程结束,不等主程序先跑了 println!("主线程:完成"); }

消息传递:channel

论"别通过共享内存通信,要通过通信共享内存"

Rust 推崇 Go 那一套:用 channel 在线程间传消息,而不是大家抢同一块内存。std::sync::mpsc 是多生产者单消费者通道:tx 发送、rx 接收。

use std::sync::mpsc; use std::thread; fn main() { let (tx, rx) = mpsc::channel(); thread::spawn(move || { tx.send(String::from("你好,主线程")).unwrap(); }); let msg = rx.recv().unwrap(); // 阻塞等消息 println!("收到:{}", msg); }
$ cargo run 收到:你好,主线程

共享状态:Mutex 与 Arc

use std::sync::{Arc, Mutex}; use std::thread; fn main() { // 多线程共享可变数据的标准组合:Arc<Mutex<T>> // Arc 负责多线程共享所有权,Mutex 负责互斥访问 let counter = Arc::new(Mutex::new(0)); let mut handles = vec![]; for _ in 0..10 { let c = Arc::clone(&counter); let h = thread::spawn(move || { let mut num = c.lock().unwrap(); // 拿锁,离开作用域自动放 *num += 1; }); handles.push(h); } for h in handles { h.join().unwrap(); } println!("最终计数:{}", *counter.lock().unwrap()); }
$ cargo run 最终计数:10

Send / Sync:并发安全的守门人

两个标记 trait(自动实现,别手写)
trait含义
Send类型能安全地把所有权转移到另一个线程。大多数类型自动是;裸指针、Rc 不是。
Sync类型能安全地被多线程共享不可变引用。T: Sync 等价于 &T: Send。RefCell、Cell 不是。

论为什么传 Rc 跨线程会编译错

因为 Rc 不是 Send——它的引用计数不是原子的,多线程同时加计数会乱。编译器直接报错,从根上挡住。多线程共享用 Arc<Mutex<T>>,别用 Rc。这就是"无畏并发"的日常体现:你想干危险的事,编译器先拦住。

其他同步工具:RwLock<T>(多读单写)、Barrier(多线程集合点)、Condvar(条件变量)、OnceLock/OnceCell(懒加载静态量)、std::sync::atomic(原子操作 AtomicBool/AtomicI32 + 内存序)。并行迭代直接用 rayon 库,把 .iter() 换成 .par_iter() 就自动多线程。

async 与 Future:单线程也能"并发"等 IO

论同步多线程 vs 异步单线程

上面的线程是操作系统级的:一个线程卡住,操作系统切到下一个,栈还得占着。异步则是用户态的:一个线程跑成千上万个任务,等网络/文件时这个任务主动让出,不占栈。Rust 里 async fn 返回一个 Future(一个"将来才就绪"的值),必须配一个异步运行时(如 Tokio)来驱动它跑。基础语法在这页,Tokio 深度实战在《Rust 后端》那页。

// 注意:async fn 不装运行时根本不会跑,只是返回一个 Future async fn fetch_data() -> String { "网络回来的数据".to_string() } // 下面这行什么都不会发生——返回的 Future 还没人 poll // fetch_data(); // 编译器会警告 "unused Future"! // 真实代码用 Tokio 驱动: // #[tokio::main] // async fn main() { // let data = fetch_data().await; // await 挂起当前任务,等它就绪 // println!("{}", data); // }

三个要点:① async fn 是惰性的,不 .await 就不执行;② .await 时任务让出 CPU,运行时去跑别人;③ 必须有运行时(Tokio)来"推"Future 往前跑。Rust 标准库只定义了 Future trait,不带运行时——这和 Go 的 goroutine 自带运行时不一样。