Fearless Concurrency in Rust
Master Fearless Concurrency in Rust: thread spawning, message passing via channels, shared state with Arc and Mutex, and the Send and Sync traits.
Fifteenth post in the intermediate Rust series. Concurrent programming has traditionally been difficult and error-prone — data races, deadlocks, and memory corruption are notorious bugs in multi-threaded systems.
Rust tackles this with the concept of Fearless Concurrency: ownership and the type system turn concurrency bugs into compile-time errors rather than runtime nightmares.
graph TD
A["Fearless Concurrency in Rust"] --> B["Thread Spawning<br/>std::thread::spawn"]
A --> C["Message Passing<br/>Channels (mpsc)"]
A --> D["Shared State<br/>Arc<T> + Mutex<T>"]
A --> E["Extensible Safety<br/>Send & Sync Traits"]
Explain Like I’m 10: The Busy Restaurant Kitchen#
Imagine a busy restaurant kitchen:
- Single-threaded: Only 1 chef does everything from chopping vegetables to cooking and dishwashing. Very safe, but slow.
- Unsafe multi-threading (C/C++): 5 chefs grab the same chopping board with no rules, accidentally cutting each other and mixing up ingredients (race conditions, data corruption).
- Rust Fearless Concurrency: Each chef has their own designated station (ownership), communicates via order conveyor belts (channels), and if they must share a rare copper pan, they lock and unlock it with a key (Arc + Mutex). The head chef (the compiler) checks all safety protocols before the kitchen even opens!
1. Using Threads to Run Code Simultaneously#
In Rust, you spawn native OS threads with thread::spawn:
use std::thread;
use std::time::Duration;
fn main() {
let handle = thread::spawn(|| {
for i in 1..=5 {
println!("Spawned thread: {i}");
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..=3 {
println!("Main thread: {i}");
thread::sleep(Duration::from_millis(1));
}
// Wait for the spawned thread to finish
handle.join().unwrap();
}rustTransferring Ownership with move Closures#
When a spawned thread needs data from the surrounding scope, use move to transfer ownership to the thread:
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(move || {
println!("Vector inside thread: {v:?}");
});
handle.join().unwrap();
}rust2. Message Passing: Channels (mpsc)#
A popular motto from the Go community rings equally true in Rust: “Do not communicate by sharing memory; instead, share memory by communicating.”
Rust provides mpsc (multiple producer, single consumer) channels in the standard library:
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
fn main() {
let (tx, rx) = mpsc::channel();
let tx1 = tx.clone(); // Clone the transmitter for a second producer
// Producer thread 1
thread::spawn(move || {
let msgs = vec!["hi", "from", "thread", "1"];
for msg in msgs {
tx1.send(String::from(msg)).unwrap();
thread::sleep(Duration::from_millis(200));
}
});
// Producer thread 2
thread::spawn(move || {
let msgs = vec!["more", "messages", "from", "thread 2"];
for msg in msgs {
tx.send(String::from(msg)).unwrap();
thread::sleep(Duration::from_millis(200));
}
});
// Consumer on the main thread
for received in rx {
println!("Got: {received}");
}
}rust3. Shared-State Concurrency: Arc<T> and Mutex<T>#
When threads need to access and mutate the same shared data:
Mutex<T>(Mutual Exclusion): Guards access so only one thread can hold the lock and access the inner value at a time.Arc<T>(Atomic Reference Counting): A thread-safe smart pointer that allows multiple threads to share ownership of theMutex.
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter_clone = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter_clone.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Final counter result: {}", *counter.lock().unwrap()); // 10
}rust[!NOTE]
Rc<T>is not thread-safe because its reference count is not updated atomically. The Rust compiler will refuse to compile code that sendsRc<T>across threads, enforcing the use ofArc<T>.
4. Extensible Safety: Send and Sync Traits#
Rust’s concurrency guarantees are built on two fundamental auto traits:
Send: Indicates that ownership of the type can be transferred between threads.Sync: Indicates that it is safe for multiple threads to access references (&T) to the type simultaneously (T: Syncimplies&T: Send).
Most Rust primitives are automatically Send and Sync. Types with non-thread-safe internal state (like Rc<T> or RefCell<T>) are marked !Send or !Sync, preventing unsafe multi-threaded usage at compile time.
Summary#
thread::spawncreates OS threads with safe ownership transfer viamoveclosures.mpsc::channeloffers safe message passing between multiple producers and a single consumer.Arc<Mutex<T>>is the standard pattern for shared, mutable state across multiple threads.SendandSynctraits empower the compiler to enforce data race prevention at compile time.