首页 / Rust 入门教程 / 消息传递

Rust 入门教程

消息传递

本教程共 78 篇 · 第 66 篇 · 更新于 2026-08-08 · 约 8 分钟阅读

RustRust 入门教程消息传递channelmpsc并发Send

本节目标:掌握用标准库 std::sync::mpsc 通道在多个线程之间传递数据,理解”用通信来共享内存”的思想、消息如何转移所有权、同步与异步通道的区别,并避开子线程导致主线程卡死的新手坑。

Go 语言有句名言:“不要通过共享内存来通信,而要通过通信来共享内存。“这句话点出了并发编程里一个核心思路:与其让多个线程抢同一块内存,不如让它们通过传递消息来交换数据。Rust 标准库就提供了消息通道(channel)来支持这种方式。

1-1 什么是消息通道

消息通道就像一根管道,一头是发送者(sender),另一头是接收者(receiver)。一个线程往管道里塞数据,另一个线程从管道另一头取数据。想象一场直播:几个主播(发送者)联合直播,观众(接收者)在屏幕前接收内容。主播和观众不需要共享同一个笔记本,他们通过直播通道协作。

Rust 标准库提供的通道叫 std::sync::mpscmpscmultiple 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! 多复制几行,你会依次看到 EmptyOk(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 转移到接收端的 receiveds 随之失效。这个设计非常安全:假如没有所有权保护,同一个字符串被两个线程同时持有,任何一个线程改了内容,另一个线程看到的就是乱的。

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 之前忘了把主线程手里那个原始的 txdrop 掉,循环就永远等不到”所有发送者都关闭”的时刻,主线程会一直卡在 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-channelflume 这样的三方库。下一章我们看另一条路:多个线程共享同一块状态,靠锁来保护。