Message Passing

Message Ownership Transfer

When a message msg: T is sent through a channel via tx.send(msg), ownership of msg is moved from the producer thread to the consumer thread. For heap-allocated containers (String, Vec<u8>, Box<T>, HashMap<K, V>), transferring ownership over a channel is a zero-copy pointer transfer: only the small stack header (24 bytes for Vec<u8> or String, 8 bytes for Box<T>) is copied into the channel slot, while the underlying heap buffer remains at the exact same memory address!

Zero-Copy Heap Buffer Transfer Across Threads

Producer Thread:
  let mut buf = Vec::with_capacity(4096);  // Heap buffer allocated at 0x600001798000
  buf.extend_from_slice(b"packet");
  tx.send(buf).unwrap();
       |
       |  Copies ONLY the 24-byte `(ptr, len, cap)` header into the channel!
       |  (Zero bytes of the 4096-byte heap buffer are copied!)
       v
Consumer Thread:
  let mut buf = rx.recv().unwrap();        // Owns the EXACT same heap address 0x600001798000!
  buf.push(b'!');                          // Can mutate in place without locking!
use std::sync::mpsc;
use std::thread;

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

    let producer = thread::spawn(move || {
        let data = vec![10_u8, 20, 30, 40];
        let heap_addr_before = data.as_ptr() as usize;
        tx.send(data).unwrap();
        heap_addr_before
    });

    let mut received = rx.recv().unwrap();
    let heap_addr_after = received.as_ptr() as usize;
    let addr_from_producer = producer.join().unwrap();

    // The consumer now uniquely owns the exact same heap allocation and can mutate it:
    received.push(50);

    println!("same heap allocation address: {}", addr_from_producer == heap_addr_after);
    println!("mutated by consumer:          {:?}", received);
}
same heap allocation address: true
mutated by consumer:          [10, 20, 30, 40, 50]

Bidirectional Recycling Pattern (Zero-Allocation Buffer Ping-Pong)

In real-time audio, video, or high-frequency networking pipelines where allocating and freeing a Vec<u8> on every message causes allocator contention, pairing a forward data channel (data_tx $\rightarrow$ data_rx) with a return recycle channel (recycle_tx $\rightarrow$ recycle_rx) transfers ownership of pre-allocated Vec<u8> buffers back and forth in a closed loop with zero heap allocations after startup:

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

fn main() {
    let (data_tx, data_rx) = mpsc::channel::<Vec<u8>>();
    let (recycle_tx, recycle_rx) = mpsc::channel::<Vec<u8>>();

    // Pre-populate the recycle pool with 1 reusable buffer of capacity 1024:
    recycle_tx.send(Vec::with_capacity(1024)).unwrap();

    let producer = thread::spawn(move || {
        for round in 1..=3 {
            // Pull a recycled buffer from the consumer:
            let mut buf = recycle_rx.recv().unwrap();
            buf.clear();
            buf.extend_from_slice(format!("frame-{round}").as_bytes());
            data_tx.send(buf).unwrap();
        }
    });

    for _ in 1..=3 {
        let buf = data_rx.recv().unwrap();
        println!(
            "consumer processed {:?} (retained capacity = {})",
            std::str::from_utf8(&buf).unwrap(),
            buf.capacity()
        );
        // Send the owned Vec back to the producer for reuse:
        let _ = recycle_tx.send(buf);
    }

    producer.join().unwrap();
}
consumer processed "frame-1" (retained capacity = 1024)
consumer processed "frame-2" (retained capacity = 1024)
consumer processed "frame-3" (retained capacity = 1024)