Message Passing — Channels and Mutex Basics
Communicate between threads with mpsc channels, share state with Arc<Mutex<T>>, and understand Rust's concurrency primitives
Message Passing — Channels and Mutex Basics
Rust follows the Go mantra: "Do not communicate by sharing memory; instead, share memory by communicating." Channels provide message-passing concurrency.
mpsc Channels
mpsc stands for Multiple Producer, Single Consumer:
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let val = String::from("hello from thread");
tx.send(val).unwrap();
// println!("{val}"); // ERROR: val was moved by send
});
let received = rx.recv().unwrap();
println!("Got: {received}");
}send and recv
| Method | Blocking? | Returns | Error When |
|---|---|---|---|
send(val) | No (bounded: yes) | Result<(), SendError> | Receiver dropped |
recv() | Yes (blocks) | Result<T, RecvError> | All senders dropped |
try_recv() | No | Result<T, TryRecvError> | Nothing yet / closed |
try_recv — Non-Blocking Receive
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_secs(1));
tx.send("done").unwrap();
});
loop {
match rx.try_recv() {
Ok(msg) => {
println!("{msg}");
break;
}
Err(mpsc::TryRecvError::Empty) => {
println!("waiting...");
thread::sleep(Duration::from_millis(100));
}
Err(mpsc::TryRecvError::Disconnected) => {
println!("channel closed");
break;
}
}
}
}Multiple Producers
Clone the sender for multiple producing threads:
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
let tx1 = tx.clone();
thread::spawn(move || {
tx1.send("msg from thread 1").unwrap();
});
let tx2 = tx.clone();
thread::spawn(move || {
tx2.send("msg from thread 2").unwrap();
});
drop(tx); // Close the original sender
for received in rx {
println!("Got: {received}");
}
}Sending Multiple Messages
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let vals = vec![
String::from("one"),
String::from("two"),
String::from("three"),
];
for val in vals {
tx.send(val).unwrap();
}
});
for received in rx {
println!("Got: {received}");
}
}Treating rx as an iterator (in for) is the most idiomatic way to receive multiple messages. It blocks on each message and stops when the channel closes.
Sending Different Types
use std::sync::mpsc;
#[derive(Debug)]
enum Message {
Text(String),
Number(i32),
Quit,
}
fn main() {
let (tx, rx) = mpsc::channel::<Message>();
let handle = std::thread::spawn(move || {
tx.send(Message::Text("hello".into())).unwrap();
tx.send(Message::Number(42)).unwrap();
tx.send(Message::Quit).unwrap();
});
for msg in rx {
match msg {
Message::Text(t) => println!("text: {t}"),
Message::Number(n) => println!("number: {n}"),
Message::Quit => {
println!("quitting");
break;
}
}
}
handle.join().unwrap();
}Mutex Basics (Preview)
Mutex provides mutual exclusion — only one thread can access the data at a time:
use std::sync::Mutex;
fn main() {
let m = Mutex::new(5);
{
let mut num = m.lock().unwrap();
*num = 6;
} // Lock released when `num` goes out of scope
println!("m = {m:?}"); // Mutex { data: 6, poisoned: false, .. }
}Mutex::lock() returns a MutexGuard that implements Deref and DerefMut. The lock is released when the guard is dropped. Never hold a lock across an .await point!
Sharing a Mutex Across Threads
use std::sync::{Mutex, Arc};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap()); // 10
}Arc — Atomic Reference Counting
Arc<T> enables shared ownership across threads:
| Feature | Rc<T> | Arc<T> |
|---|---|---|
| Thread-safe | No | Yes |
| Performance | Fast | Slower (atomic ops) |
| Clone behavior | Non-atomic increment | Atomic increment |
| Use case | Single-threaded | Multi-threaded |
use std::sync::Arc;
use std::thread;
fn main() {
let data = Arc::new(vec![1, 2, 3]);
let mut handles = vec![];
for i in 0..5 {
let data = Arc::clone(&data);
handles.push(thread::spawn(move || {
println!("Thread {i}: {:?}", data);
}));
}
for handle in handles {
handle.join().unwrap();
}
}Real-World: Worker Pool with Channels
use std::sync::mpsc;
use std::thread;
type Job = Box<dyn FnOnce() + Send + 'static>;
struct ThreadPool {
sender: mpsc::Sender<Job>,
workers: Vec<thread::JoinHandle<()>>,
}
impl ThreadPool {
fn new(size: usize) -> Self {
let (sender, receiver) = mpsc::channel();
let receiver = std::sync::Arc::new(std::sync::Mutex::new(receiver));
let mut workers = Vec::with_capacity(size);
for id in 0..size {
let receiver = Arc::clone(&receiver);
let worker = thread::spawn(move || loop {
let job = receiver.lock().unwrap().recv();
match job {
Ok(job) => {
println!("Worker {id} executing job");
job();
}
Err(_) => {
println!("Worker {id} shutting down");
break;
}
}
});
workers.push(worker);
}
ThreadPool { sender, workers }
}
fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
self.sender.send(Box::new(f)).unwrap();
}
}
impl Drop for ThreadPool {
fn drop(&mut self) {
for worker in self.workers.drain(..) {
worker.join().unwrap();
}
}
}
fn main() {
let pool = ThreadPool::new(4);
for i in 0..8 {
pool.execute(move || {
println!("Processing task {i}");
});
}
drop(pool);
println!("All tasks complete");
}Practice Questions
- What does
mpscstand for and what does it imply? - How do you send a value through a channel?
- How do you receive a value from a channel?
- What's the difference between
recv()andtry_recv()? - How do you create multiple producers for one channel?
- What happens to the channel when all senders are dropped?
- How does
Mutexensure exclusive access? - Why do you need
Arcto share aMutexacross threads? - What's the difference between
RcandArc? - How do you send messages of different types through a single channel?