Skip to content

Expert · Concurrency and async · topic 36 of 50 · 25 min

Message passing with channels

Explanation

A channel sends values from one thread to another. std::sync::mpsc::channel() returns a Sender (tx) and a Receiver (rx); mpsc means multiple producers, single consumer. send moves the value into the channel, so the sender cannot touch it afterwards. Clone tx for several producers, and loop over rx to receive until every sender is dropped.

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.

Think of it like this: A channel is like the order window between waiters and the kitchen in a restaurant. Many waiters (producers) push order slips through the window; one cook (the consumer) takes them in order. Once a slip is pushed through, the waiter no longer has it.
rust
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}");
}
tx (transmitter) sends, rx (receiver) receives. send moves ownership of the String into the channel.

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

rust
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}");
    }
}
Iterating over rx receives until the channel closes. Order between waiters can vary.
Careful: If you forget drop(tx) for the original sender, the for loop over rx never ends: the channel stays open because one Sender is still alive in main. This "hang" is a very common bug.

What the types say

ItemMeaning
mpscMultiple 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

Send data, do not share it

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.

rust
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
}
The logger stops when every Sender has been dropped.

Practice (0/3 done)

Task 1 · Question

A program loops "for msg in rx" and never exits after all workers finish. What is the likely cause?

Task 2 · Question

What does mpsc stand for?

Task 3 · Practice

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

Question 1

1.What happens to a String after tx.send(s)?

Question 2

2.How do several threads send on the same channel?

Question 3

3.When does rx.recv() return an Err?

Question 4

4.Why use mpsc::sync_channel(n)?

Read more (free)

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.

From Channels: passing data between goroutines

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.

From select: waiting on several channels

Many threads read a config and rarely update it. Which is a good fit?Related

Answer: RwLock

RwLock allows many readers at once.

From Shared state: Arc, Mutex, Send and Sync

Why are immutable objects thread-safe?Related

Answer: Their state never changes, so there is nothing to race on

No writes, no races.

From Atomic variables and concurrent collections

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

  1. 2 more topics in “Concurrency and async” before its exam: Shared state: Arc, Mutex, Send and Sync, Async Rust with tokio.
  2. Module 9 exam: Concurrency and async · 13 questions · 25 min, without help. 4 topics of this module still to finish first.
  3. Then the next module, Testing, tooling and unsafe Rust. 50 topics left in the course before the final exam.

← Previous: Threads