Message Passing

Channels

A channel is a thread-safe communication conduit through which one or more transmitter endpoints (Sender<T> / SyncSender<T>) send typed values T to a receiver endpoint (Receiver<T>) in First-In, First-Out (FIFO) order.

Anatomy of a Channel

Creating a channel returns a tuple (tx, rx) representing its two coupled halves. If either half is dropped—all transmitters are dropped, or the receiver is dropped—the channel transitions to a disconnected state and notifies the remaining half on its next operation.

Channel Architecture:

  Producer Thread(s)                     FIFO Buffer Queue                 Consumer Thread
  +------------------+                 +--------------------+              +------------------+
  | tx: Sender<T> ---+-- tx.send(v) -> | [ msg_3 | msg_2 ] -+-- rx.recv()->| rx: Receiver<T>  |
  +------------------+                 +--------------------+              +------------------+

Unbounded (channel) vs. Bounded (sync_channel) Channels

The standard library provides two channel constructors in std::sync::mpsc:

Channel Kind Constructor Transmitter Type Queue Capacity .send(val) Behavior
Asynchronous (Unbounded) mpsc::channel::<T>() Sender<T> Dynamically grows in heap memory Never blocks the sending thread
Synchronous (Bounded) mpsc::sync_channel::<T>(cap) SyncSender<T> Fixed upper bound cap elements Blocks the sending thread when cap items are already queued (backpressure)
Rendezvous (cap == 0) mpsc::sync_channel::<T>(0) SyncSender<T> 0 buffered slots Blocks until a consumer thread simultaneously calls .recv() (direct handoff!)
use std::sync::mpsc;
use std::thread;

fn main() {
    // 1. Unbounded channel (`mpsc::channel`): `.send()` returns immediately
    let (tx, rx) = mpsc::channel::<&'static str>();
    tx.send("buffered without waiting").unwrap();
    println!("unbounded recv: {:?}", rx.recv().unwrap());

    // 2. Bounded channel (`mpsc::sync_channel(2)`): enforces a capacity of 2 items
    let (sync_tx, sync_rx) = mpsc::sync_channel::<u32>(2);
    sync_tx.send(10).unwrap();
    sync_tx.send(20).unwrap();
    // Queue is now full (2/2); `try_send` returns `Err(TrySendError::Full(30))` without blocking:
    println!("try_send on full bounded channel: {:?}", sync_tx.try_send(30));

    // Rendezvous channel (`cap == 0`): sender and receiver meet at the exact handoff point
    let (rdv_tx, rdv_rx) = mpsc::sync_channel::<&'static str>(0);
    thread::scope(|s| {
        s.spawn(move || {
            rdv_tx.send("direct rendezvous handoff").unwrap();
        });
        println!("rendezvous recv: {:?} (queued = {:?})", rdv_rx.recv().unwrap(), sync_rx.recv().unwrap());
    });
}
unbounded recv: "buffered without waiting"
try_send on full bounded channel: Err(Full(30))
rendezvous recv: "direct rendezvous handoff" (queued = 10)

Why Bounded Channels Are Critical for Production Backpressure

With an unbounded mpsc::channel(), if producer threads generate messages faster than the consumer thread can process them (for example, logging events during a traffic spike or reading network packets faster than disk writes complete), the in-memory queue grows without limit until the process runs out of RAM. Using mpsc::sync_channel(capacity) applies automatic backpressure by slowing down producers when the queue reaches capacity (or allows dropping load via .try_send()).