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)