🎓 A1Learner
🔍

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 hands

Example

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?