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()).