Blocking Receives
A blocking receive (rx.recv(), rx.recv_timeout(dur), or iterating over rx) parks the calling consumer thread in the operating system scheduler whenever the channel queue is empty, waking the thread automatically as soon as a producer sends a message or the channel disconnects.
Comparing Blocking Receive Idioms on Receiver<T>
| Idiom | Consumes rx? |
Blocks When Empty? | Stops When |
|---|---|---|---|
rx.recv() |
No (&self) |
Indefinitely | 1 message arrives (Ok(T)) or all Senders drop (Err(RecvError)) |
rx.recv_timeout(dur) |
No (&self) |
Up to dur |
1 message arrives, dur elapses (Timeout), or channel disconnects (Disconnected) |
for msg in &rx (rx.iter()) |
No (&self) |
Between messages | All Senders drop and queue empties (leaves rx usable afterward) |
for msg in rx (rx.into_iter()) |
Yes (self) |
Between messages | All Senders drop and queue empties (drops rx when loop exits) |
Consumer Thread State During `for msg in &rx`:
[ Queue Non-Empty ] ---> Pops front message immediately in user space (`0` syscalls!)
[ Queue Empty, Senders Alive ] ---> Parks thread via OS futex (`0%` CPU usage while waiting)
[ Queue Empty, 0 Senders Alive ] -> `Iterator::next` returns `None`, exiting `for` loop!
Periodic Heartbeat Loops with recv_timeout
When a worker thread needs to block waiting for incoming commands and run periodic maintenance (such as flushing a batch buffer, checking a health timer, or logging status) whenever no command arrives within a window, rx.recv_timeout(Duration) handles both without spinning:
use std::sync::mpsc::{self, RecvTimeoutError};
use std::thread;
use std::time::Duration;
fn main() {
let (tx, rx) = mpsc::channel::<&'static str>();
let producer = thread::spawn(move || {
tx.send("job-1").unwrap();
thread::sleep(Duration::from_millis(15));
tx.send("job-2").unwrap();
// `tx` drops here, disconnecting the channel.
});
let mut timeouts_observed = 0;
loop {
match rx.recv_timeout(Duration::from_millis(5)) {
Ok(job) => println!("processed {:?}", job),
Err(RecvTimeoutError::Timeout) => {
timeouts_observed += 1;
}
Err(RecvTimeoutError::Disconnected) => {
println!(
"channel disconnected cleanly (observed {} idle timeouts)",
timeouts_observed > 0
);
break;
}
}
}
producer.join().unwrap();
}
processed "job-1"
processed "job-2"
channel disconnected cleanly (observed true idle timeouts)
Batching Optimization: recv() + try_iter()
In high-throughput I/O consumers (such as a database batch writer or log flusher), waking up and issuing a separate disk write per message is inefficient. A classic optimization is calling blocking rx.recv() to sleep until the first item arrives, and then immediately chaining non-blocking rx.try_iter().take(MAX_BATCH - 1) to scoop up any additional items already sitting in the queue before flushing the entire batch in one operation:
use std::sync::mpsc;
fn receive_next_batch<T>(rx: &mpsc::Receiver<T>, max_batch: usize) -> Option<Vec<T>> {
// 1. Block until at least ONE item arrives (or return `None` if disconnected):
let first = rx.recv().ok()?;
// 2. Non-blockingly drain up to `max_batch - 1` additional queued items:
let mut batch = Vec::with_capacity(max_batch);
batch.push(first);
batch.extend(rx.try_iter().take(max_batch - 1));
Some(batch)
}
fn main() {
let (tx, rx) = mpsc::channel::<u32>();
for n in 1..=5 {
tx.send(n).unwrap();
}
drop(tx);
while let Some(batch) = receive_next_batch(&rx, 3) {
println!("flushing batch of {} items: {:?}", batch.len(), batch);
}
}
flushing batch of 3 items: [1, 2, 3]
flushing batch of 2 items: [4, 5]