消息传递
本教程共 78 篇 · 第 66 篇 · 更新于 2026-08-08 · 约 8 分钟阅读
本节目标:掌握用标准库
std::sync::mpsc通道在多个线程之间传递数据,理解”用通信来共享内存”的思想、消息如何转移所有权、同步与异步通道的区别,并避开子线程导致主线程卡死的新手坑。
Go 语言有句名言:“不要通过共享内存来通信,而要通过通信来共享内存。“这句话点出了并发编程里一个核心思路:与其让多个线程抢同一块内存,不如让它们通过传递消息来交换数据。Rust 标准库就提供了消息通道(channel)来支持这种方式。
1-1 什么是消息通道
消息通道就像一根管道,一头是发送者(sender),另一头是接收者(receiver)。一个线程往管道里塞数据,另一个线程从管道另一头取数据。想象一场直播:几个主播(发送者)联合直播,观众(接收者)在屏幕前接收内容。主播和观众不需要共享同一个笔记本,他们通过直播通道协作。
Rust 标准库提供的通道叫 std::sync::mpsc。mpsc 是 multiple producer, single consumer 的缩写,意思是”多生产者、单消费者”:你可以有多个发送者,但只能有一个接收者。这覆盖了大多数常见场景。
1-2 创建通道并收发消息
use std::sync::mpsc;
use std::thread;
fn main() {
// 创建一个通道,返回 (发送者, 接收者)
let (tx, rx) = mpsc::channel();
// 创建子线程,通过 move 把发送者交过去
thread::spawn(move || {
tx.send(1).unwrap();
});
// 主线程接收并打印
println!("收到: {}", rx.recv().unwrap());
}
几个关键点:
mpsc::channel()返回一个元组(tx, rx),tx是发送者,rx是接收者。编译器会根据tx.send(1)自动推导出它们是mpsc::Sender<i32>和mpsc::Receiver<i32>。一旦类型确定,这个通道就只认i32,塞别的类型会编译报错。rx.recv()会阻塞当前线程,直到收到一个值,或者通道被关闭。- 发送者必须通过
move交给子线程,因为它要离开主线程去另一个线程工作。
Note
send方法返回Result,因为如果接收者已经被丢弃,再发消息就没有意义了,此时会返回错误。上面用unwrap快速处理,真实项目里你需要更认真地对待这个错误。
1-3 不阻塞的 try_recv
recv 会死等,有时候你不希望线程卡住。这时可以用 try_recv,它尝试收一次,没消息立刻返回一个错误,不阻塞:
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
tx.send(1).unwrap();
});
println!("尝试接收: {:?}", rx.try_recv());
}
由于子线程创建需要时间,主线程的 try_recv 往往先执行,此时消息还没发出,于是会得到一个 Err(Empty),表示通道是空的。等子线程真把消息发出来、再发完被丢弃后,你还可能看到 Err(Disconnected),表示发送者已经关了。
Tip想体验一下,可以把
println!多复制几行,你会依次看到Empty、Ok(1)、Disconnected的变化过程。
1-4 消息转移所有权
通道也要遵守 Rust 的所有权规则。如果消息类型实现了 Copy(比如 i32),那发过去的是一份拷贝;如果没实现 Copy(比如 String),所有权就直接转移给接收端,发送端不能再用了。
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let s = String::from("我飞走啦");
tx.send(s).unwrap();
// 下面这行会报错:borrow of moved value
// println!("s 还在吗: {}", s);
});
let received = rx.recv().unwrap();
println!("收到: {}", received);
}
String 底层数据存在堆上,没有 Copy。发送之后,所有权从发送端的 s 转移到接收端的 received,s 随之失效。这个设计非常安全:假如没有所有权保护,同一个字符串被两个线程同时持有,任何一个线程改了内容,另一个线程看到的就是乱的。
1-5 用 for 循环持续接收
实际中我们经常要连续收很多消息。接收者 rx 实现了 Iterator,所以可以用 for 循环来收:
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("thread"),
];
for val in vals {
tx.send(val).unwrap();
thread::sleep(Duration::from_secs(1));
}
});
for received in rx {
println!("收到: {}", received);
}
}
子线程一边发一边睡,主线程用 for 阻塞地迭代接收。当子线程结束、发送者 tx 被丢弃时,循环自动终止,主线程顺利结束。
1-6 多个发送者
一个发送者被 move 进线程后就没了,想要多个线程发消息,得先把发送者 clone 一份:
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
let tx1 = tx.clone();
thread::spawn(move || {
tx.send(String::from("来自原始发送者")).unwrap();
});
thread::spawn(move || {
tx1.send(String::from("来自克隆发送者")).unwrap();
});
for received in rx {
println!("收到: {}", received);
}
}
只有所有发送者都被丢弃,接收者才会收到”结束”信号、跳出 for 循环。这个 clone 不在热路径上,只发生一次,性能上完全不用操心。
Warning新手最容易踩的坑:如果你在
for received in rx之前忘了把主线程手里那个原始的tx也drop掉,循环就永远等不到”所有发送者都关闭”的时刻,主线程会一直卡在for上。记住,自己手里那份发送者也要drop,循环才能正常结束。
1-7 同步通道与异步通道
标准库的 mpsc::channel() 创建的是异步通道:发送者发消息时,不管有没有人在收,都不阻塞,消息先堆在通道里。
如果你想让发送变成阻塞式的(必须有人收了,发送才算完成),用 mpsc::sync_channel(缓冲大小)。那个参数是缓冲条数:设为 0 表示没有缓冲,发一条就必须等一条被收走;设为 N 表示可以无阻塞地先发 N 条,缓冲满了才会阻塞。
异步通道缓冲上限取决于你的内存,可以无限发,但有内存暴涨的风险。需要严格控制内存时,用带缓冲的同步通道更安全。
关于消息顺序多说一句:虽然哪个线程先发不确定,但同一个发送者发出的消息,接收顺序和发送顺序一致(FIFO,先进先出)。顺序不是乱的,只是”谁先发”不确定。
1-8 通道何时关闭
很多人会问:通道用不用手动关?答案是不用。当所有发送者都被丢弃,或者接收者被丢弃时,通道自动关闭,这件事在编译期就安排好了,没有运行期开销。所以如果你克隆了多个发送者,必须全部被丢弃,接收端的 for 循环才会收到结束信号并退出;只要还有一个发送者(比如主线程手里那份原始的 tx)活着,for 循环就一直等。
这也正好解释了本章开头那个坑:忘了 drop 原始发送者,主线程就会卡在接收上,最后一行永远不打印。理解”关闭 = 所有发送者都 drop”这条规则,能帮你避开百分之八十的通道相关卡死问题。写循环接收时,务必确认发送者都会被释放,必要时显式 drop(tx)。
Note通道关闭是自动的,但”谁还拿着发送者”要你自己心里有数。多发送者场景下,养成
clone完立刻drop原始发送者的习惯,能省掉很多调试时间。
1-9 小结
消息传递是”避免共享内存”的并发思路在 Rust 里的落地。标准库 mpsc 通道一端发、一端收,消息按所有权规则转移,发送者全部关闭后接收循环自然结束。还有 sync_channel 这种带缓冲、会阻塞的变体可供选择。如果后面你需要”多接收者”或更高性能,社区里有 crossbeam-channel、flume 这样的三方库。下一章我们看另一条路:多个线程共享同一块状态,靠锁来保护。