Channel Disconnection
A channel becomes disconnected when either all transmitter handles (Sender<T> / SyncSender<T>) have been dropped or the unique Receiver<T> handle has been dropped. Because mpsc tracks the live count of transmitters and the receiver automatically via Drop, channel disconnection acts as a built-in, lock-free graceful shutdown signal across threads.
Asymmetric Behavior on Disconnection
Crucially, dropping all Senders does not discard messages that were already sent and sitting in the channel buffer! The Receiver<T> drains every buffered message in FIFO order first, and only reports disconnection after the buffer is completely empty:
Case 1: All `Sender<T>` Clones Are Dropped (Normal Producer Shutdown)
+-----------------------------------------------------------------------+
| 1. `rx.recv()` / `rx.try_recv()` continues returning `Ok(msg)` for |
| all buffered messages remaining in the channel queue! |
| 2. Once the queue is empty, `rx.recv()` returns `Err(RecvError)` |
| (and `for msg in rx` terminates cleanly with `None`!). |
+-----------------------------------------------------------------------+
Case 2: The `Receiver<T>` Is Dropped Early (Consumer Cancelled / Exited)
+-----------------------------------------------------------------------+
| 1. Any buffered messages still inside the channel are dropped. |
| 2. Any subsequent `tx.send(val)` on a remaining `Sender<T>` returns |
| `Err(SendError(val))` immediately so the producer can stop work! |
+-----------------------------------------------------------------------+
The Dangling Original Sender Hang Trap
When spawning $N$ producer threads in a loop by cloning let tx_clone = tx.clone();, the original tx created in the parent thread remains alive after the loop!
If the parent thread then runs for msg in rx { ... } without calling drop(tx) first (or moving tx into the last spawned thread), the for loop will process all worker messages and then hang forever—waiting for the parent thread's own unused tx to send a message or drop!
Why Forgetting `drop(tx)` Hangs `for msg in rx`:
main thread:
let (tx, rx) = mpsc::channel();
for _ in 0..2 {
let tx_c = tx.clone();
thread::spawn(move || tx_c.send(1)); // `tx_c` drops when worker exits
}
// ⚠️ `tx` is STILL ALIVE here on `main` (sender_count == 1)!
for msg in rx { ... } // Blocks forever after 2 messages because `tx` never drops!
Either drop tx explicitly (drop(tx);) after spawning all workers, or construct the workers inside a block scope that moves tx:
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel::<&'static str>();
thread::spawn({
let tx = tx.clone();
move || {
tx.send("msg 1").unwrap();
tx.send("msg 2").unwrap();
}
});
// Drop the original `tx` so the channel disconnects as soon as the worker exits:
drop(tx);
// Drains "msg 1" and "msg 2", then exits the loop cleanly on disconnection:
for msg in &rx {
println!("drained before disconnect: {:?}", msg);
}
// Now that the queue is empty and all Senders are dropped, `recv()` returns `Err(RecvError)`:
println!("subsequent rx.recv(): {:?}", rx.recv());
}
drained before disconnect: "msg 1"
drained before disconnect: "msg 2"
subsequent rx.recv(): Err(RecvError)
Early Consumer Exit Propagating Back to Producers
Conversely, if a consumer only needs the first $K$ results and then drops rx, producers checking if tx.send(item).is_err() { break; } automatically abort their remaining expensive work as soon as rx is dropped.