Expert · Concurrency and async · topic 36 of 50 · 25 min
Message passing with channels
Explanation
Sharing by communicating
There are two main ways for threads to cooperate: share memory with locks (next topic), or send messages. Many developers find messages easier to reason about: each piece of data belongs to one thread at a time, and moves to another thread by being sent. A channel is the pipe that carries these messages.
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let order = String::from("2 masala chai");
tx.send(order).unwrap();
// println!("{order}"); // error: borrow of moved value: order
});
let received = rx.recv().unwrap(); // waits until a message arrives
println!("kitchen got: {received}");
}recv() blocks until a message arrives. It returns Err when all senders have been dropped and the channel is empty, meaning no more messages can ever come. try_recv() does not wait: it returns immediately with a message or an error.
Notice that the channel is typed. mpsc::channel() creates a Sender<T> and a Receiver<T> for one type T, which Rust usually infers from the first send. You cannot send a String on one line and a number on the next; if you need different kinds of messages, send an enum with one variant per kind.
Several producers
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
for waiter in 1..=3 {
let tx = tx.clone(); // each thread gets its own Sender
thread::spawn(move || {
tx.send(format!("order from waiter {waiter}")).unwrap();
});
}
drop(tx); // drop the original Sender
for msg in rx { // ends when all Senders are gone
println!("{msg}");
}
}What the types say
| Item | Meaning |
|---|---|
| mpsc | Multiple producer, single consumer |
| Sender<T> | Can be cloned and sent to many threads |
| Receiver<T> | Only one; cannot be cloned |
| send(v) | Moves v into the channel; Err if the receiver is gone |
| recv() | Waits for the next value; Err when the channel is closed and empty |
| mpsc::sync_channel(n) | A bounded channel: send waits when n messages are queued |
mpsc::channel() is unbounded: it can grow without limit if the consumer is slower than the producers. sync_channel(n) adds back-pressure: producers wait when the queue is full, which protects memory. For multiple consumers, crates such as crossbeam-channel provide more channel types; async code uses tokio's channels.
Common mistakes
- A program that never ends: a Sender is still alive (often the original tx in main). Drop it.
- Trying to clone the Receiver. mpsc has one consumer; use a different design or another crate for many consumers.
- Using a value after sending it: send moves it. Send a clone if you still need it.
- Unbounded queues growing without limit under load. Use sync_channel for back-pressure.
Need help?
Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.
Try:
Watch the lesson
Message passing with channels · 1/6
Threads can cooperate by sending messages through a channel, instead of sharing memory.
Prefer a video by a teacher? Find free videos on “rust mpsc channel message passing threads tutorial” on YouTube ↗
In real life: A log collector for many workers
A video-processing service runs several worker threads that encode videos. Instead of every worker writing to the log file at once (which would mix lines), each worker sends log messages through a channel to one logger thread that writes them in order. Only the logger owns the file, so there is no need for locks.
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel::<String>();
let logger = thread::spawn(move || {
let mut lines = 0;
for line in rx {
println!("[log] {line}");
lines += 1;
}
lines
});
let workers: Vec<_> = (1..=3).map(|id| {
let tx = tx.clone();
thread::spawn(move || tx.send(format!("worker {id} finished video")).unwrap())
}).collect();
for w in workers { w.join().unwrap(); }
drop(tx);
println!("{} lines logged", logger.join().unwrap()); // 3 lines logged
}Practice (0/3 done)
A program loops "for msg in rx" and never exits after all workers finish. What is the likely cause?
What does mpsc stand for?
Simulate 3 cash counters at a supermarket, each in its own thread, sending (counter_id, bill_amount) for 5 bills each through a channel. The main thread receives all bills and prints the total per counter and the grand total. Make sure the program ends.
Quick quiz
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Shared state: Arc, Mutex, Send and SyncConcurrency and async · same module
- Async Rust with tokioConcurrency and async · same module
- Channels: passing data between goroutinesGo Programming (backend and cloud) · Concurrency: goroutines, channels and context
- select: waiting on several channelsGo Programming (backend and cloud) · Concurrency: goroutines, channels and context
- Atomic variables and concurrent collectionsJava Programming (Core to Spring Boot) · Multithreading and concurrency
- Concurrency 2: atomics, async, futures and condition variablesC++ Programming (modern C++ and DSA) · Exceptions, concurrency and building projects with CMake
Questions people ask about this
Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.
What happens to a String after tx.send(s)?
Answer: Ownership moves into the channel; s cannot be used
send takes the value by move.
How do several threads send on the same channel?
Answer: Clone the Sender for each thread
Sender can be cloned; each clone can move into a thread.
When does rx.recv() return an Err?
Answer: When all Senders are dropped and no messages are left
Err signals a closed, empty channel.
Why use mpsc::sync_channel(n)?
Answer: To limit queued messages so producers wait (back-pressure)
A bounded channel protects memory when consumers are slow.
What is the type of a receive-only channel of ints?Related
Answer: <-chan int
<-chan int can only receive; chan<- int can only send.
Why set a channel variable to nil inside a fan-in loop?Related
Answer: Because operations on a nil channel block forever, so that case is disabled
A nil channel case can never be selected.
Many threads read a config and rarely update it. Which is a good fit?Related
Why are immutable objects thread-safe?Related
Answer: Their state never changes, so there is nothing to race on
No writes, no races.
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.
Next steps
- 2 more topics in “Concurrency and async” before its exam: Shared state: Arc, Mutex, Send and Sync, Async Rust with tokio.
- Module 9 exam: Concurrency and async · 13 questions · 25 min, without help. 4 topics of this module still to finish first.
- Then the next module, Testing, tooling and unsafe Rust. 50 topics left in the course before the final exam.