Rust Channels
A channel is a postbox that carries messages between threads. Channels replace sharing data through locks with passing messages.
channel() makes a matched pair
mpsc means "many producers, one consumer". That is many senders and exactly one receiver. send moves the value in. recv() waits until a message arrives:
use std::sync::mpsc;
fn main() {
let (tx, rx) = mpsc::channel();
tx.send(42).unwrap();
println!("received {}", rx.recv().unwrap());
}send moves the value — ownership travels
use std::sync::mpsc;
fn main() {
let (tx, rx) = mpsc::channel();
let word = String::from("owned now by the receiver");
tx.send(word).unwrap();
// println!("{word}"); // would not compile: word moved into the channel
println!("{}", rx.recv().unwrap());
}try_recv checks without waiting
Use it in a loop when the thread has other work to do between checks, instead of blocking inside recv:
use std::sync::mpsc;
fn main() {
let (tx, rx) = mpsc::channel::<i32>();
println!("empty? {}", rx.try_recv().is_err());
tx.send(7).unwrap();
println!("now: {}", rx.try_recv().unwrap());
}Read until every sender is dropped
Reading with for msg in rx ends by itself when every sender has been dropped and the channel is empty. A channel is closed when either half is dropped:
use std::sync::mpsc;
fn main() {
let (tx, rx) = mpsc::channel();
tx.send("one").unwrap();
tx.send("two").unwrap();
drop(tx);
for msg in rx {
println!("got {msg}");
}
println!("channel closed");
}send fails when the receiver is gone
send gives back a Result. It is an Err when the receiver has already been dropped. There is nowhere left to deliver to:
use std::sync::mpsc;
fn main() {
let (tx, rx) = mpsc::channel::<i32>();
drop(rx);
println!("send worked = {}", tx.send(1).is_ok());
}A sync_channel adds a waiting room
The plain channel() is unbounded. So send never waits. But its queue can grow without limit if the receiver falls behind. sync_channel(n) caps how many messages may wait. So fast senders slow down. That slowing down is back-pressure.
let (tx, rx) = mpsc::sync_channel(2); // at most 2 messages may wait
let (tx0, rx0) = mpsc::sync_channel(0); // no waiting room at all:
// every send waits until a recv takes the value out of the sender's handsExample
use std::sync::mpsc; use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || { tx.send("hello from thread").unwrap(); });
println!("got: {}", rx.recv().unwrap());
}Key points
- Sending hands the value over: it is the receiver's now, not the sender's.
- Clone the sender to give more threads a way to send
- Use rx.recv_timeout(duration) when a wait should not be endless.
- After the duration, it returns an Err instead of a message.
- So a stuck sender shows up as a report.
- It does not hang in silence.
- A channel moves a message from one thread to another instead of sharing the data behind a lock.
In this example, channel() creates a matched pair: tx for sending, rx for receiving. The new thread owns tx (moved in) and sends the text "hello from thread". The send moves the String into the channel. On the main thread, rx.recv() waits — it blocks — until the message arrives, then returns it. unwrap() takes the text out of the Ok, and the println! shows got: hello from thread. No shared variables were touched by two threads. The Rust Deadlock Detection lesson at the end of this section builds a watchdog on exactly this.
Note
For heavier jobs, the crossbeam crate offers channels with several consumers and a select! tool for waiting on many channels at once.
Try it Yourself
Edit the code, then press Run.
Running is simulated · Rust Playground
Exercise
When does a for msg in rx { } loop end?