Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help


x-i18n: generated_at: “2026-03-01T14:39:53Z” model: gemini-3-flash-preview provider: google-gemini-cli source_hash: 883ed745d87151fa820cd337420a24f59cf8391d4ce87d2ee22af3bd278e05d1 source_path: ch16-02-message-passing.md workflow: 16

通过消息传递在线程间转移数据 (Transfer Data Between Threads with Message Passing)

确保安全并发的一种日益流行的方法是消息传递,即线程或 actor 通过相互发送包含数据的消息来进行通信。这里有来自 Go 语言文档的一个口号:“不要通过共享内存来通信;相反,通过通信来共享内存。”

One increasingly popular approach to ensuring safe concurrency is message passing, where threads or actors communicate by sending each other messages containing data. Here’s the idea in a slogan from the Go language documentation: “Do not communicate by sharing memory; instead, share memory by communicating.”

为了实现消息发送并发,Rust 的标准库提供了一个通道(channels)的实现。“通道 (channel)”是一个通用的编程概念,数据通过它从一个线程发送到另一个线程。

To accomplish message-sending concurrency, Rust’s standard library provides an implementation of channels. A channel is a general programming concept by which data is sent from one thread to another.

你可以把编程中的通道想象成一条有方向的水道,比如小溪或河流。如果你把一个橡皮鸭之类的东西放入河流,它会顺流而下到达水道的末端。

You can imagine a channel in programming as being like a directional channel of water, such as a stream or a river. If you put something like a rubber duck into a river, it will travel downstream to the end of the waterway.

一个通道有两个部分:发送端和接收端。发送端是你在河流上游放入橡皮鸭的地方,接收端是橡皮鸭最终到达下游的地方。你代码的一部通过数据调用发送端的方法,另一部分检查接收端是否有到来的消息。如果发送端或接收端中的任何一个被丢弃,则称通道被“关闭”了。

A channel has two halves: a transmitter and a receiver. The transmitter half is the upstream location where you put the rubber duck into the river, and the receiver half is where the rubber duck ends up downstream. One part of your code calls methods on the transmitter with the data you want to send, and another part checks the receiving end for arriving messages. A channel is said to be closed if either the transmitter or receiver half is dropped.

在这里,我们将逐步构建一个程序,它有一个线程生成值并将其发送到通道,另一个线程接收这些值并打印出来。我们将使用通道在线程之间发送简单的值来说明此功能。一旦你熟悉了这种技术,你就可以将通道用于任何需要相互通信的线程,例如聊天系统或多个线程执行部分计算并将结果发送到一个汇总线程的系统。

Here, we’ll work up to a program that has one thread to generate values and send them down a channel, and another thread that will receive the values and print them out. We’ll be sending simple values between threads using a channel to illustrate the feature. Once you’re familiar with the technique, you could use channels for any threads that need to communicate with each other, such as a chat system or a system where many threads perform parts of a calculation and send the parts to one thread that aggregates the results.

首先,在示例 16-6 中,我们将创建一个通道但不执行任何操作。请注意,这目前还无法编译,因为 Rust 无法判断我们想通过通道发送什么类型的值。

First, in Listing 16-6, we’ll create a channel but not do anything with it. Note that this won’t compile yet because Rust can’t tell what type of values we want to send over the channel.

{{#rustdoc_include ../listings/ch16-fearless-concurrency/listing-16-06/src/main.rs}}

我们使用 mpsc::channel 函数创建一个新通道; mpsc 代表“多个生产者,单个消费者 (multiple producer, single consumer)”。简而言之,Rust 标准库实现通道的方式意味着一个通道可以有多个产生值的“发送”端,但只能有一个消费这些值的“接收”端。想象一下多条小溪汇成一条大河:所有通过任何小溪发送的东西最终都会汇入尽头的一条河中。我们目前先从单个生产者开始,但当我们让这个例子运行起来后,我们会添加多个生产者。

We create a new channel using the mpsc::channel function; mpsc stands for multiple producer, single consumer. In short, the way Rust’s standard library implements channels means a channel can have multiple sending ends that produce values but only one receiving end that consumes those values. Imagine multiple streams flowing together into one big river: Everything sent down any of the streams will end up in one river at the end. We’ll start with a single producer for now, but we’ll add multiple producers when we get this example working.

mpsc::channel 函数返回一个元组,其第一个元素是发送端(发射器),第二个元素是接收端(接收器)。缩写 txrx 在许多领域中传统上分别用于“发射器 (transmitter)”和“接收器 (receiver)”,因此我们这样命名变量来指示每一端。我们正在使用带有模式的 let 语句来解构元组;我们将在第 19 章讨论 let 语句中模式的使用和解构。目前,只需知道以这种方式使用 let 语句是提取 mpsc::channel 返回的元组各部分的便捷方法。

The mpsc::channel function returns a tuple, the first element of which is the sending end—the transmitter—and the second element of which is the receiving end—the receiver. The abbreviations tx and rx are traditionally used in many fields for transmitter and receiver, respectively, so we name our variables as such to indicate each end. We’re using a let statement with a pattern that destructures the tuples; we’ll discuss the use of patterns in let statements and destructuring in Chapter 19. For now, know that using a let statement in this way is a convenient approach to extract the pieces of the tuple returned by mpsc::channel.

让我们将发送端移动到一个生成的线程中,并让它发送一个字符串,以便生成的线程与主线程通信,如示例 16-7 所示。这就像在河流上游放入一只橡皮鸭,或者从一个线程向另一个线程发送聊天消息。

Let’s move the transmitting end into a spawned thread and have it send one string so that the spawned thread is communicating with the main thread, as shown in Listing 16-7. This is like putting a rubber duck in the river upstream or sending a chat message from one thread to another.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch16-fearless-concurrency/listing-16-07/src/main.rs}}
}

同样,我们使用 thread::spawn 创建一个新线程,然后使用 movetx 移动到闭包中,以便生成的线程拥有 tx 。生成的线程需要拥有发送端才能通过通道发送消息。

Again, we’re using thread::spawn to create a new thread and then using move to move tx into the closure so that the spawned thread owns tx. The spawned thread needs to own the transmitter to be able to send messages through the channel.

发送端有一个接收我们要发送的值的 send 方法。 send 方法返回一个 Result<T, E> 类型,所以如果接收端已经被丢弃并且没有地方可以发送值,发送操作将返回一个错误。在这个例子中,我们调用 unwrap 以便在出错时引发恐慌。但在实际应用程序中,我们会对其进行妥善处理:回到第 9 章回顾正确错误处理的策略。

The transmitter has a send method that takes the value we want to send. The send method returns a Result<T, E> type, so if the receiver has already been dropped and there’s nowhere to send a value, the send operation will return an error. In this example, we’re calling unwrap to panic in case of an error. But in a real application, we would handle it properly: Return to Chapter 9 to review strategies for proper error handling.

在示例 16-8 中,我们将从主线程的接收端获取该值。这就像从河流末端的水中取回橡皮鸭,或者接收一条聊天消息。

In Listing 16-8, we’ll get the value from the receiver in the main thread. This is like retrieving the rubber duck from the water at the end of the river or receiving a chat message.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch16-fearless-concurrency/listing-16-08/src/main.rs}}
}

接收端有两个有用的方法: recvtry_recv 。我们使用的是 recv (receive 的缩写),它将阻塞主线程的执行并等待,直到有一个值被发送到通道。一旦发送了值, recv 将在 Result<T, E> 中返回它。当发送端关闭时, recv 将返回一个错误,表示不再有值到来。

The receiver has two useful methods: recv and try_recv. We’re using recv, short for receive, which will block the main thread’s execution and wait until a value is sent down the channel. Once a value is sent, recv will return it in a Result<T, E>. When the transmitter closes, recv will return an error to signal that no more values will be coming.

try_recv 方法不会阻塞,而是会立即返回一个 Result<T, E> :如果有一个消息可用,则返回一个持有该消息的 Ok 值;如果这一次没有任何消息,则返回一个 Err 值。如果线程在等待消息的同时还有其他工作要做,使用 try_recv 很有用:我们可以编写一个循环,每隔一段时间调用一次 try_recv ,如果有消息可用就处理它,否则就先做一会儿其他工作,直到再次检查。

The try_recv method doesn’t block, but will instead return a Result<T, E> immediately: an Ok value holding a message if one is available and an Err value if there aren’t any messages this time. Using try_recv is useful if this thread has other work to do while waiting for messages: We could write a loop that calls try_recv every so often, handles a message if one is available, and otherwise does other work for a little while until checking again.

为了简单起见,我们在本例中使用了 recv ;主线程除了等待消息之外没有其他工作要做,因此阻塞主线程是合适的。

We’ve used recv in this example for simplicity; we don’t have any other work for the main thread to do other than wait for messages, so blocking the main thread is appropriate.

当我们运行示例 16-8 中的代码时,我们将看到主线程打印出的值:

When we run the code in Listing 16-8, we’ll see the value printed from the main thread:

Got: hi

完美!

Perfect!

通过通道转移所有权 (Transferring Ownership Through Channels)

Transferring Ownership Through Channels

所有权规则在消息发送中起着至关重要的作用,因为它们可以帮助你编写安全的并发代码。在整个 Rust 程序中考虑所有权的好处就是可以防止并发编程中的错误。让我们做一个实验,展示通道和所有权如何协同工作以防止问题:我们将尝试在将 val 值发送到通道“之后”在生成的线程中使用它。尝试编译示例 16-9 中的代码,看看为什么这种代码是不允许的。

The ownership rules play a vital role in message sending because they help you write safe, concurrent code. Preventing errors in concurrent programming is the advantage of thinking about ownership throughout your Rust programs. Let’s do an experiment to show how channels and ownership work together to prevent problems: We’ll try to use a val value in the spawned thread after we’ve sent it down the channel. Try compiling the code in Listing 16-9 to see why this code isn’t allowed.

{{#rustdoc_include ../listings/ch16-fearless-concurrency/listing-16-09/src/main.rs}}

在这里,我们尝试在通过 tx.sendval 发送到通道后打印它。允许这样做会是个坏主意:一旦该值被发送到另一个线程,那个线程可能会在我们尝试再次使用该值之前修改或丢弃它。潜在地,由于数据不一致或不存在,另一个线程的修改可能会导致错误或意外结果。然而,如果我们尝试编译示例 16-9 中的代码,Rust 会给我们一个错误:

Here, we try to print val after we’ve sent it down the channel via tx.send. Allowing this would be a bad idea: Once the value has been sent to another thread, that thread could modify or drop it before we try to use the value again. Potentially, the other thread’s modifications could cause errors or unexpected results due to inconsistent or nonexistent data. However, Rust gives us an error if we try to compile the code in Listing 16-9:

{{#include ../listings/ch16-fearless-concurrency/listing-16-09/output.txt}}

我们的并发错误导致了编译时错误。 send 函数获取其参数的所有权,当值移动时,接收端就获取了它的所有权。这防止了我们在发送该值后意外地再次使用它;所有权系统会检查一切是否正常。

Our concurrency mistake has caused a compile-time error. The send function takes ownership of its parameter, and when the value is moved the receiver takes ownership of it. This stops us from accidentally using the value again after sending it; the ownership system checks that everything is okay.

发送多个值 (Sending Multiple Values)

Sending Multiple Values

示例 16-8 中的代码编译并运行了,但它并没有清晰地向我们展示两个独立的线程正在通过通道相互交谈。

The code in Listing 16-8 compiled and ran, but it didn’t clearly show us that two separate threads were talking to each other over the channel.

在示例 16-10 中,我们做了一些修改,这将证明示例 16-8 中的代码正在并发运行:生成的线程现在将发送多条消息,并在每条消息之间暂停一秒。

In Listing 16-10, we’ve made some modifications that will prove the code in Listing 16-8 is running concurrently: The spawned thread will now send multiple messages and pause for a second between each message.

{{#rustdoc_include ../listings/ch16-fearless-concurrency/listing-16-10/src/main.rs}}

这一次,生成的线程有一个我们想要发送到主线程的字符串向量。我们遍历它们,逐个发送,并在每个之间通过调用 thread::sleep 函数并传入一秒的 Duration 值来暂停。

This time, the spawned thread has a vector of strings that we want to send to the main thread. We iterate over them, sending each individually, and pause between each by calling the thread::sleep function with a Duration value of one second.

在主线程中,我们不再显式调用 recv 函数:相反,我们将 rx 视为迭代器。对于接收到的每个值,我们都将其打印出来。当通道关闭时,迭代将结束。

In the main thread, we’re not calling the recv function explicitly anymore: Instead, we’re treating rx as an iterator. For each value received, we’re printing it. When the channel is closed, iteration will end.

当运行示例 16-10 中的代码时,你应该看到以下输出,每行之间有一秒的暂停:

When running the code in Listing 16-10, you should see the following output with a one-second pause in between each line:

Got: hi
Got: from
Got: the
Got: thread

因为我们在主线程的 for 循环中没有任何暂停或延迟的代码,所以我们可以看出主线程正在等待从生成的线程接收值。

Because we don’t have any code that pauses or delays in the for loop in the main thread, we can tell that the main thread is waiting to receive values from the spawned thread.

创建多个生产者 (Creating Multiple Producers)

Creating Multiple Producers

早些时候我们提到 mpsc 是“多个生产者,单个消费者”的缩写。让我们应用 mpsc 并扩展示例 16-10 中的代码,创建多个线程,它们都向同一个接收端发送值。我们可以通过克隆发送端来实现这一点,如示例 16-11 所示。

Earlier we mentioned that mpsc was an acronym for multiple producer, single consumer. Let’s put mpsc to use and expand the code in Listing 16-10 to create multiple threads that all send values to the same receiver. We can do so by cloning the transmitter, as shown in Listing 16-11.

{{#rustdoc_include ../listings/ch16-fearless-concurrency/listing-16-11/src/main.rs:here}}

这一次,在我们创建第一个生成线程之前,我们在发送端上调用了 clone 。这将给我们一个新的发送端,我们可以将其传递给第一个生成线程。我们将原始发送端传递给第二个生成线程。这给了我们两个线程,每个线程都向那个唯一的接收端发送不同的消息。

This time, before we create the first spawned thread, we call clone on the transmitter. This will give us a new transmitter we can pass to the first spawned thread. We pass the original transmitter to a second spawned thread. This gives us two threads, each sending different messages to the one receiver.

当你运行代码时,你的输出应该看起来像这样:

When you run the code, your output should look like something this:

Got: hi
Got: more
Got: from
Got: messages
Got: for
Got: the
Got: thread
Got: you

你可能会看到值的另一种顺序,这取决于你的系统。这就是并发之所以有趣也之所以困难的原因。如果你尝试使用 thread::sleep ,在不同的线程中给它赋予各种不同的值,那么每次运行都会更具非确定性,并且每次都会产生不同的输出。

You might see the values in another order, depending on your system. This is what makes concurrency interesting as well as difficult. If you experiment with thread::sleep, giving it various values in the different threads, each run will be more nondeterministic and create different output each time.

既然我们已经研究了通道的工作原理,让我们来看看另一种不同的并发方法。

Now that we’ve looked at how channels work, let’s look at a different method of concurrency.