mpsc Channels
Send between threads.
mpsc Channels is a free Learn Rust Coding lesson on CoddyKit — lesson 1 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Learn Rust Coding learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
What Is a Channel?
A channel is a one-way pipe for sending values from one thread to another. Rust's standard library provides std::sync::mpsc where mpsc means multiple producer, single consumer.
- The Sender half pushes values in.
- The Receiver half pulls values out.
Channels let threads communicate by passing messages instead of sharing memory directly, which avoids many data races.
Creating a Channel
Call mpsc::channel() to get a tuple of (Sender, Receiver). Here we send one value from a spawned thread back to the main thread.
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
tx.send(42).unwrap();
});
let received = rx.recv().unwrap();
println!("Got: {}", received);
}send and recv
tx.send(value) returns a Result: it fails only if the receiver has been dropped. rx.recv() blocks until a value arrives, returning Err when all senders are gone.
sendmoves ownership of the value into the channel.recvtakes ownership out on the other side.
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let msg = String::from("hello from thread");
tx.send(msg).unwrap();
});
let text = rx.recv().unwrap();
println!("{}", text);
}Ownership Moves Through the Channel
Because send takes the value by value, you cannot use it after sending. This compile-time rule guarantees no thread keeps a stale reference to data now owned by another thread.
Below, trying to print msg after send would be a compile error, so we only use it once.
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let data = vec![1, 2, 3];
tx.send(data).unwrap();
// data is moved; cannot use it here
});
let v = rx.recv().unwrap();
println!("Sum: {}", v.iter().sum::<i32>());
}Iterating Over a Receiver
A Receiver implements IntoIterator. Looping over it yields each value until the channel closes (all senders dropped). This is the idiomatic way to consume a stream of messages.
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
for i in 1..=3 {
tx.send(i).unwrap();
}
});
for received in rx {
println!("Received: {}", received);
}
}Multiple Producers with clone
The mp in mpsc means you can have many senders. Clone the Sender and give a copy to each thread. The receiver collects everything until every clone is dropped.
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
let tx2 = tx.clone();
thread::spawn(move || { tx.send("from A").unwrap(); });
thread::spawn(move || { tx2.send("from B").unwrap(); });
for msg in rx {
println!("{}", msg);
}
}Channel Closing Semantics
The receiver loop ends automatically when all senders are dropped. If even one Sender stays alive, for msg in rx blocks forever waiting for more. Always drop or scope senders correctly to let the loop finish.
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
let handle = thread::spawn(move || {
for i in 0..3 {
tx.send(i * 10).unwrap();
}
// tx dropped here, closing the channel
});
handle.join().unwrap();
let total: i32 = rx.iter().sum();
println!("Total: {}", total);
}try_recv for Non-Blocking Reads
recv blocks, but try_recv returns immediately with a Result. It gives Ok(value) if a message is ready, or Err if the channel is empty or disconnected. Useful in event loops that must keep doing other work.
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
thread::sleep(Duration::from_millis(50));
tx.send("ready").unwrap();
});
loop {
match rx.try_recv() {
Ok(msg) => { println!("{}", msg); break; }
Err(_) => println!("waiting..."),
}
thread::sleep(Duration::from_millis(20));
}
}Sending Custom Types
Any type that is Send can travel through a channel, including your own structs and enums. Enums are great for modeling distinct message kinds in a worker protocol.
use std::sync::mpsc;
use std::thread;
enum Job {
Print(String),
Add(i32, i32),
}
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
tx.send(Job::Print(String::from("hi"))).unwrap();
tx.send(Job::Add(2, 3)).unwrap();
});
for job in rx {
match job {
Job::Print(s) => println!("print: {}", s),
Job::Add(a, b) => println!("add: {}", a + b),
}
}
}sync_channel and Backpressure
mpsc::sync_channel(n) creates a bounded channel with buffer size n. When the buffer is full, send blocks until space frees up. This gives you backpressure, preventing a fast producer from overwhelming a slow consumer.
sync_channel(0)is a rendezvous channel: send and recv must meet.
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::sync_channel(2);
thread::spawn(move || {
for i in 1..=4 {
tx.send(i).unwrap();
println!("sent {}", i);
}
});
for v in rx {
println!("got {}", v);
}
}A Simple Worker Pattern
Channels shine for the producer/consumer pattern: one thread produces work items, another consumes and processes them. Here the main thread produces numbers and a worker squares each one and reports back over a second channel.
use std::sync::mpsc;
use std::thread;
fn main() {
let (job_tx, job_rx) = mpsc::channel();
let (res_tx, res_rx) = mpsc::channel();
thread::spawn(move || {
for n in job_rx {
res_tx.send(n * n).unwrap();
}
});
for n in 1..=4 {
job_tx.send(n).unwrap();
}
drop(job_tx);
for r in res_rx {
println!("square: {}", r);
}
}Quick Check
Test your understanding of mpsc channels.
Recap
You learned the core of mpsc channels:
mpsc::channel()returns a(Sender, Receiver)pair.sendmoves a value in;recvblocks to take it out.- Iterating a receiver consumes messages until all senders drop.
- Clone the
Senderfor multiple producers. try_recvis non-blocking;sync_channel(n)adds bounded backpressure.
Channels let threads share data safely by passing ownership rather than sharing memory.
Frequently asked questions
Is the “mpsc Channels” lesson free?
Yes — the full text of “mpsc Channels” is free to read here on the web, and the Learn Rust Coding course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Learn Rust Coding course, upgrade to CoddyKit PRO.
What will I learn in “mpsc Channels”?
Send between threads. You practise Learn Rust Coding with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start Learn Rust Coding?
No prior experience is required. Learn Rust Coding on CoddyKit is structured for beginners through advanced learners; this is — lesson 1 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “mpsc Channels” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this Learn Rust Coding lesson?
Yes. Every Learn Rust Coding lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.