Skip to content

Channels and Message Passing

Channels implement the actor model — concurrent tasks communicate by sending messages rather than Sharing memory. Rust provides several channel types, each optimized for different communication Patterns. The sender and receiver are separate endpoints; messages are moved from sender to Receiver, transferring ownership.

TypeProducersConsumersBufferingUse Case
std::sync::mpscMultipleSingleBounded/UnboundedSimple work distribution
tokio::sync::mpscMultipleSingleBounded/UnboundedAsync work distribution
oneshotSingleSingleNoneSingle response
broadcastSingleMultipleBoundedPub/sub notifications
watchSingleMultipleSingle valueConfiguration updates

The standard library”s channel is synchronous (blocking) and designed for OS threads:

use std::sync::mpsc;
use std::thread;
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let val = String::from("hello");
tx.send(val).unwrap();
// val is moved — no longer accessible here
});
let received = rx.recv().unwrap();
assert_eq!(received, "hello");

Clone the sender to create multiple producers:

use std::sync::mpsc;
use std::thread;
let (tx, rx) = mpsc::channel();
let tx1 = tx.clone();
thread::spawn(move || {
tx.send("from thread 1").unwrap();
});
thread::spawn(move || {
tx1.send("from thread 2").unwrap();
});
drop(tx);
for received in rx {
println!("got: {}", received);
}

When all senders are dropped, recv() returns Err and the iterator terminates.

use std::sync::mpsc;
let (tx, rx) = mpsc::channel(); // unbounded — grows as needed
let (tx, rx) = mpsc::sync_channel(10); // bounded — capacity 10
MethodBlocking?Returns
tx.send(val)Yes (if bounded and full)Result<(), SendError<T>>
rx.recv()Yes (if empty and senders exist)Result<T, RecvError>
rx.try_recv()NoResult<T, TryRecvError>
rx.recv_timeout(dur)Yes (with timeout)Result<T, RecvTimeoutError>

Tokio’s async channel uses .await instead of blocking:

use tokio::sync::mpsc;
#[tokio::main]
async fn main() {
let (tx, mut rx) = mpsc::channel(32);
tokio::spawn(async move {
tx.send("hello").await.unwrap();
});
while let Some(msg) = rx.recv().await {
println!("{}", msg);
}
}
use tokio::sync::mpsc;
let (tx, rx) = mpsc::channel(32); // bounded — capacity 32
let (tx, rx) = mpsc::unbounded_channel(); // unbounded — grows as needed
## Intuition

Channels are Rust’s message-passing primitive, inspired by Go’s CSP model. mpsc channels allow multiple producers but single consumption. Crossbeam provides multi-producer multi-consumer channels with better performance. Messages are moved through channels, transferring ownership and preventing shared state. This pattern logically serializes access to shared resources without locks, and the type system ensures messages cannot be used after being sent.

  • [[rust/06-concurrency/concurrency]] - Thread creation and management
  • [[rust/02-ownership-borrowing/ownership]] - Ownership transfer through channels
  • [[rust/05-traits-generics/traits-and-generics]] - Generic channel type parameters
  • [[rust/04-error-handling/error-handling]] - Error propagation in concurrent code