Skip to main content
In this lesson, we’ll explore message passing in Rust, a powerful model for handling concurrency that enables safe communication between threads. By sending and receiving messages rather than sharing data directly, you can minimize the risk of data races while adhering to Rust’s ownership and borrowing principles. When working with message passing in Rust, channels serve as the primary mechanism. They allow threads to send values to each other in a safe and efficient manner.

What Is a Channel?

A channel in Rust is a communication primitive that enables threads to pass messages between one another. Think of it as a pipeline: one end (the sender) pushes data into the channel, while the other end (the receiver) pulls data out.
The image is a diagram about "Message Passing" in programming, highlighting concepts of "Ownership" and "Borrowing," with a focus on doing so "Safely" and "Efficiently."
Rust provides the std::sync::mpsc module—where MPSC stands for “multiple producers, single consumer”—as the standard way to create channels. This allows multiple threads to send messages into the channel while a single thread is designated to receive them. Below is a simple example illustrating how to create a channel:
In this code, tx represents the sender and rx represents the receiver. While this MPSC channel is suitable for many message-passing scenarios, Rust also supports more complex configurations like multi-consumer channels when needed.
The image illustrates Rust's channel mechanism, highlighting "Multiple-Producer, Single-Consumer" with sections for "Multi-Producer" and "Multi-Consumer Channels."

Creating and Using Channels

Channels in Rust are created with the channel function, which returns a tuple containing a sender and a receiver. Here’s how you can create a channel for sending String messages:
If the type of messages is ambiguous, clear type annotations or sending an initial message can help the compiler infer the correct type. For example, to send a message using the channel:
The send method transfers ownership of the value into the channel. If the receiver is not present, this method will return an error.
When running the program above, you might see a warning about the unused variable rx. To suppress this warning, either use the receiver in your code or prefix it with an underscore (i.e., _rx).

Receiving Messages

On the receiving side, you can use the blocking recv method to wait for a message:
In the example above, rx.recv() blocks the current thread until a message is available, and the message is then printed to the console. Alternatively, if you prefer a non-blocking approach, you can use try_recv. This method immediately returns an error if no message has arrived yet:
Running either of these examples should produce the following output:

Message Passing Between Threads

A common scenario for using channels is communicating between threads. Consider this example, where a child thread sends a message to the main thread:
Notice how the move keyword is used to transfer ownership of the sender tx into the child thread. Attempting to use tx in the main thread after the move would result in a compiler error. To ensure proper synchronization, you can capture the thread handle and call join to wait for the thread to complete:

Cloning the Sender for Multiple Producers

When you need multiple threads to send messages to a single receiver, you can clone the sender. The example below spawns five threads, each sending an integer to the receiver:
A sample output might be:
Since thread execution is non-deterministic, the order of messages may vary. Alternatively, you can let the receiver’s iterator run until all sender handles are dropped, automatically ending the loop once all messages have been processed:
Once all messages are received and all sender handles go out of scope, the receiver’s iterator exits, and the program terminates gracefully.

Recap

Message passing in Rust offers a safe and efficient mechanism to manage concurrency by transferring ownership through channels. Use this approach when you need to coordinate work among several threads without sharing mutable state. Its seamless integration with Rust’s ownership and borrowing model makes it an essential tool in your Rust programming toolkit. For more detailed information, consider checking out Rust’s official documentation on channels.

Watch Video