Why Are std::sync::mpsc::channel Senders the Way They Are?

⚓ Rust    📅 2026-03-26    👤 surdeus    👁️ 3      

surdeus

My understanding of senders created through channel() is that (and this will be important later) the compiler determines the type of the content being sent over the channel on the first use of send in the source code. Fine, ok, strongly typed language prefers determining types at compile time, makes sense, BUT send also transfers ownership of the type which seems… at odds with my understanding of OS implemented channels (like pipes) in both unix and windows.

In OS provided versions of channels, the paradigm is to copy the data into memory managed by the send and recv functions associated with that pipe. Naturally this is shared memory so, as a rule of thumb, it outlives any stack frame referencing it during the lifetime of the program. This is where my cognitive dissonance comes in.

In another language (ex: c/c++) if I create a variable local to a stack frame which in turn is local to a thread, when I “send” that data over a channel, in rust terms, I am always “cloning” that data because that data exists within a stack frame that may not exist by the time the receiver goes to recv that data (which is why data is usually copied for send and recv in OS pipes) but in rust I’m… not doing that? The only thing I can think of where transferring ownership through tx.send() is even close to a zero cost abstraction is if rust is doing some magic where data sent over the channel is preemptively marked by the compiler for its initial allocation to be on the heap and the send call is transferring a reference under the hood (which would explain why the compiler would want to know the type so much, it makes transferring that way much easier) but that still has issues because allocating to the heap instead of the stack is not necessarily zero cost.

A transfer of ownership in rust implies (in this case at least) that I move that data out of my scope and into another scope never for me to operate on again, but if rust is implementing a channel the same way an OS is, and not the aforementioned compiler magic, the data is being copied into the shared memory of the the channel in order to be transferred so it must still exist immediately after that copy in the thread local stack frame after the send so why remove ownership if its still there? Why not just require the the type inferred by send implement clone and take a clone-able reference? Also, if the compiler is removing that data from the stack for some reason (when it doesn’t need to) how is that a zero overhead abstraction?

Of course the first answer in response is “just do a manual clone like tx.send(data.clone() why is this even a question” but that too has issues. The clone function is happening within the context of the stack frame of the function it is being called in, so would the data being cloned not also be copied at least initially onto the thread local stack? And in that case the work of clone is happening twice, once for the clone() call which creates a stack frame local region of memory allocated to that cloned value, and once when send is transferring ownership of that cloned value copying it across ends.

This brings me back to the title: why are channels like this, compiler magic or something else?

13 posts - 7 participants

Read full topic

🏷️ Rust_feed