</>
Skip to content
Rust lessons (41/43)

Rust — Channels

"Share memory by communicating"

Rust concurrency philosophy: instead of threads fighting over shared memory, pass ownership of messages between them. Channels are the pipes.

use std::sync::mpsc;      // multi-producer, single-consumer
use std::thread;

fn main() {
    let (tx, rx) = mpsc::channel();     // transmitter + receiver

    thread::spawn(move || {
        tx.send("hello from worker").unwrap();
    });

    println!("{}", rx.recv().unwrap()); // blocks until a message arrives
}

send MOVES the value into the channel — the sending thread loses it instantly. Ownership literally travels through the pipe:

let val = String::from("data");
tx.send(val).unwrap();
// println!("{}", val);   ❌ moved into the channel!

That's why data races via channel misuse can't happen — only one side ever owns a message.

recv vs try_recv

MethodBehavior
rx.recv()blocks until message or channel closed → Result
rx.try_recv()returns immediately: Ok(msg) / Err(Empty) / Err(Disconnected)

Multiple messages — treat rx as an iterator

for received in rx {
    println!("got {}", received);
}

The loop ends automatically when ALL transmitters drop (channel closes):

let (tx, rx) = mpsc::channel();

thread::spawn(move || {
    for n in 1..=5 {
        tx.send(n).unwrap();
        thread::sleep(std::time::Duration::from_millis(100));
    }
});                                  // tx dropped here when thread ends

for received in rx {                 // runs 5 times, then exits cleanly
    println!("received {}", received);
}

Keep tx alive in main and the loop waits forever instead — lifetime of senders controls closure of receivers.

Multiple producers

mpsc = multi-producer: clone the transmitter for each worker:

let (tx, rx) = mpsc::channel();

for id in 0..3 {
    let tx = tx.clone();             // each thread gets its own handle
    thread::spawn(move || {
        tx.send(format!("worker {} done", id)).unwrap();
    });
}

drop(tx);                            // release original so rx can finish

for msg in rx {
    println!("{}", msg);
}

The explicit drop(tx) matters: with the original still alive in main, for msg in rx would never end even after workers finish.

When channels beat shared state

✓ Worker pools distributing jobs
✓ Streaming results back to a collector
✓ Event systems between components
✗ Shared counters/cache → Arc<Mutex<T>> fits better

Rust book's motto: share memory by communicating. Design systems as message-passers first; reach for Mutex only when sharing is genuinely simpler.

Gotchas: forgetting to close/drop tx hangs receivers · send() errors when receiver dropped ("receiver disconnected") · mpsc receiver is NOT clonable — one consumer; crossbeam-channel lifts that limit.

Mini Practice

  1. Send three strings across a channel; print on arrival.
  2. Producer with sleep delays; watch iterator block per message.
  3. Three producer threads + one consumer collecting all results.
  4. Demonstrate send-fails-after-drop error.
  5. Rewrite the Arc<Mutex> counter exercise using a channel aggregator.

Next: testing →

Related Topics

Frequently Asked Questions about Channels

What is Channels in Rust?

Channels is a fundamental concept in Rust. This lesson explains it step by step with clear examples, making it easy for beginners to understand.

How do I learn Channels?

Start by reading the explanation above, then try the code examples. Practice by modifying the examples and experimenting with different values. Hands-on practice is the best way to learn Channels.

Why is Channels important in Rust?

Channels is essential for Rust development. Understanding this concept will help you write better code and solve real-world problems more effectively.