Channels
Message passing in TypeScript vs Rust
Section titled “Message passing in TypeScript vs Rust”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.
std::sync::mpsc
Section titled “std::sync::mpsc”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
Senderto create multiple producers (hence “multi-producer”). - There is exactly one
Receiver. - When all
Senders are dropped, the channel is closed andrx.recv()returnsErr.
// TypeScript / Node.js: Worker message passingimport { 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');}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}Multiple producers (fan-in pattern)
Section titled “Multiple producers (fan-in pattern)”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: fan-in with Promise.allasync function fanIn(workers: number): Promise<string[]> { const results = await Promise.all( Array.from({ length: workers }, (_, i) => Promise.resolve(`message from worker ${i}`) ) ); return results;}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}"); }}Try it
Section titled “Try it”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}"); }}Compiling…