Message Passing

std.sync.mpsc

std::sync::mpsc (Multi-Producer, Single-Consumer) is the standard library module providing thread-safe message-passing channels. Its internal implementation (powered by the crossbeam-channel algorithm in modern rustc) uses lock-free array-backed linked blocks for high throughput and low allocation overhead.

Core Types in std::sync::mpsc

Type Role Thread-Safety Traits (Send / Sync / Clone)
Sender<T> Sending half of an unbounded channel() Send + Sync + Clone (when T: Send)
SyncSender<T> Sending half of a bounded sync_channel(cap) Send + Sync + Clone (when T: Send)
Receiver<T> Single receiving half for both channel kinds Send (when T: Send), !Sync, !Clone
SendError<T> Error returned when .send(val) fails because Receiver was dropped Unwraps original message val: T via .0
TrySendError<T> Error returned by SyncSender::try_send Variants: Full(T) or Disconnected(T)
RecvError Error returned by .recv() when all Senders have dropped and queue is empty Unit struct
TryRecvError Error returned by .try_recv() Variants: Empty or Disconnected
RecvTimeoutError Error returned by .recv_timeout() Variants: Timeout or Disconnected
Iter<'_, T> / TryIter<'_, T> / IntoIter<T> Blocking and non-blocking iterators over received messages Created via .iter(), .try_iter(), .into_iter()

Why Sender<T> and SyncSender<T> Are Send + Sync While Receiver<T> Is !Sync

In modern Rust (stabilized in Rust 1.72), both Sender<T> and SyncSender<T> implement Send and Sync (whenever T: Send), so threads can either clone a Sender<T> or share &Sender<T> directly across scoped threads.

In contrast, Receiver<T> is Send (can be moved to a dedicated consumer thread) but !Sync and !Clone, because the internal consumer cursor is designed for a single reader without multi-consumer lock overhead.

use std::sync::mpsc;
use std::thread;

fn main() {
    let (tx, rx) = mpsc::channel::<u32>();

    // Because `Sender<T>: Sync` (when `T: Send`), scoped threads can even borrow `&tx`
    // directly without cloning `tx`!
    thread::scope(|s| {
        for i in 1..=3 {
            let tx_ref = &tx;
            s.spawn(move || {
                tx_ref.send(i * 100).unwrap();
            });
        }
    });

    // Drop `tx` so the receiver iterator knows no more messages will arrive:
    drop(tx);

    let mut received: Vec<u32> = rx.into_iter().collect();
    received.sort();
    println!("received via mpsc: {:?}", received);
}
received via mpsc: [100, 200, 300]

Recovering Unsent Data from SendError<T>

If the Receiver<T> has already been dropped when a producer calls tx.send(value), the channel does not drop or lose value—it returns Err(SendError(value)) so the producer can recover ownership of value (err.0) and log, retry, or persist it locally:

use std::sync::mpsc;

fn main() {
    let (tx, rx) = mpsc::channel::<String>();
    drop(rx); // Receiver disconnects immediately

    let important_msg = String::from("unsent audit record");
    if let Err(send_err) = tx.send(important_msg) {
        let recovered: String = send_err.0;
        println!("recovered unsent message from SendError: {:?}", recovered);
    }
}
recovered unsent message from SendError: "unsent audit record"