Skip to content

Channels

One of the safest ways to coordinate concurrency is message passing: instead of sharing memory, threads communicate by sending values through a channel. Go popularized this model with goroutines and channels. Rust provides the same idea in the standard library via std::sync::mpsc.

In JavaScript, message passing is the only way to communicate across Worker boundaries — postMessage sends a serialized copy of the data. Rust channels are different: they transfer ownership of the value. The sending thread gives up the value; the receiving thread owns it exclusively. No copy, no lock.

mpsc stands for multi-producer, single-consumer. You create a channel with mpsc::channel(), which returns a (Sender<T>, Receiver<T>) pair.

  • You can clone the Sender to create multiple producers (hence “multi-producer”).
  • There is exactly one Receiver.
  • When all Senders are dropped, the channel is closed and rx.recv() returns Err.
TypeScript
// TypeScript / Node.js: Worker message passing
import { Worker, isMainThread, parentPort, workerData } from 'worker_threads';
if (isMainThread) {
const worker = new Worker(__filename, { workerData: 0 });
// Messages arrive asynchronously via event
worker.on('message', (msg: string) => {
console.log('received:', msg);
});
} else {
// Worker sends a message back to the main thread
// Data is COPIED (structured clone), not transferred
parentPort?.postMessage('hello from worker');
parentPort?.postMessage('second message');
}
Rust
use std::sync::mpsc;
use std::thread;
fn main() {
// mpsc::channel() returns (Sender<T>, Receiver<T>)
let (tx, rx) = mpsc::channel::<String>();
// Spawn a thread that sends two messages then exits
thread::spawn(move || {
// tx is moved into the thread — it owns the sender
tx.send(String::from("hello from thread")).unwrap();
tx.send(String::from("second message")).unwrap();
// tx is dropped here, closing the channel
});
// recv() blocks until a message arrives, or returns Err when closed
for msg in rx { // rx.into_iter() drains until channel closes
println!("received: {msg}");
}
// Loop exits automatically when all senders are dropped
}

The “multi-producer” part of mpsc lets you clone the Sender and distribute it to many threads. All threads write into the same receiver. This is the classic fan-in pattern.

TypeScript
// TypeScript: fan-in with Promise.all
async function fanIn(workers: number): Promise<string[]> {
const results = await Promise.all(
Array.from({ length: workers }, (_, i) =>
Promise.resolve(`message from worker ${i}`)
)
);
return results;
}
Rust
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel::<String>();
let workers = 3;
let mut handles = vec![];
for i in 0..workers {
let tx2 = tx.clone(); // each thread gets its own Sender clone
let h = thread::spawn(move || {
tx2.send(format!("message from worker {i}")).unwrap();
});
handles.push(h);
}
// Drop the original sender — when all clones are also dropped,
// the channel closes and the for-loop below terminates.
drop(tx);
for h in handles { h.join().unwrap(); }
let mut msgs: Vec<String> = rx.iter().collect();
msgs.sort(); // sort for deterministic output
for m in &msgs { println!("{m}"); }
}
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel::<String>();
let workers = 3;
let mut handles = vec![];
for i in 0..workers {
let tx2 = tx.clone();
let h = thread::spawn(move || {
tx2.send(format!("message from worker {i}")).unwrap();
});
handles.push(h);
}
drop(tx);
for h in handles {
h.join().unwrap();
}
let mut msgs: Vec<String> = rx.iter().collect();
msgs.sort();
for m in &msgs {
println!("{m}");
}
}
What does 'mpsc' stand for in std::sync::mpsc?
What happens when all Sender clones are dropped?
How does Rust's channel differ from JS's postMessage?