Why "Fearless" Concurrency?
Rust's ownership system prevents data races at compile time. In languages like C++ or Java, data races are runtime bugs that are notoriously hard to reproduce and fix. In Rust, the compiler simply won't let you write code that has data races. This is what makes Rust concurrency "fearless."
Rust's Concurrency Guarantees
- No Data Races: The type system prevents simultaneous mutable access from multiple threads
- Send Trait: Types that can be transferred between threads
- Sync Trait: Types that can be shared between threads via references
Threads with std::thread
use std::thread;
use std::time::Duration;
fn main() {
// Spawn a thread
let handle = thread::spawn(|| {
for i in 1..5 {
println!("Spawned thread: {i}");
thread::sleep(Duration::from_millis(10));
}
});
for i in 1..3 {
println!("Main thread: {i}");
thread::sleep(Duration::from_millis(10));
}
// Wait for the spawned thread to finish
handle.join().unwrap();
println!("Both threads done!");
// Move data into a thread
let data = vec![1, 2, 3];
let handle = thread::spawn(move || {
// move keyword transfers ownership to the thread
println!("Thread got: {:?}", data);
let sum: i32 = data.iter().sum();
sum // Return value from thread
});
// data is no longer available here — it was moved
let result = handle.join().unwrap();
println!("Thread returned: {result}");
// Spawn multiple threads
let mut handles = vec![];
for i in 0..5 {
handles.push(thread::spawn(move || {
println!("Thread {i} running");
i * i
}));
}
let results: Vec = handles
.into_iter()
.map(|h| h.join().unwrap())
.collect();
println!("Results: {:?}", results);
}
Message Passing with Channels
use std::sync::mpsc; // Multiple Producer, Single Consumer
use std::thread;
fn main() {
// Create a channel
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let messages = vec![
String::from("hello"),
String::from("from"),
String::from("the"),
String::from("thread"),
];
for msg in messages {
tx.send(msg).unwrap();
thread::sleep(std::time::Duration::from_millis(100));
}
// tx is dropped here — channel closes
});
// Receive messages (blocks until message arrives)
for received in rx {
println!("Got: {received}");
}
// Multiple producers
let (tx, rx) = mpsc::channel();
let tx2 = tx.clone(); // Clone the sender
thread::spawn(move || {
tx.send("from thread 1").unwrap();
});
thread::spawn(move || {
tx2.send("from thread 2").unwrap();
});
for msg in rx {
println!("Received: {msg}");
}
}
Shared State with Mutex<T> and Arc<T>
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
// Mutex provides interior mutability with locking
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 || {
// lock() blocks until the lock is available
let mut num = counter.lock().unwrap();
*num += 1;
// Lock is automatically released when num goes out of scope
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Counter: {}", *counter.lock().unwrap()); // 10
// More complex shared state
let data = Arc::new(Mutex::new(Vec::new()));
let mut handles = vec![];
for i in 0..5 {
let data = Arc::clone(&data);
handles.push(thread::spawn(move || {
let mut vec = data.lock().unwrap();
vec.push(i);
}));
}
for h in handles {
h.join().unwrap();
}
let result = data.lock().unwrap();
println!("Shared vec: {:?}", *result);
}
Send and Sync Traits
// Send: ownership can be transferred between threads
// - Most types are Send
// - Rc is NOT Send (use Arc instead)
// - Raw pointers are NOT Send
// Sync: can be referenced from multiple threads
// - T is Sync if &T is Send
// - Mutex is Sync (that's its purpose!)
// - RefCell is NOT Sync (use Mutex instead)
// - Rc is NOT Sync (use Arc instead)
use std::sync::{Arc, Mutex};
use std::thread;
// The compiler enforces Send + Sync automatically!
// This won't compile because Rc is not Send:
// use std::rc::Rc;
// let data = Rc::new(5);
// thread::spawn(move || {
// println!("{}", data); // ERROR: Rc cannot be sent between threads
// });
// Fix: use Arc instead
fn thread_safe_example() {
let data = Arc::new(5);
let data_clone = Arc::clone(&data);
let handle = thread::spawn(move || {
println!("Thread sees: {}", data_clone); // OK!
});
handle.join().unwrap();
println!("Main sees: {}", data);
}
Avoiding Deadlocks
Rust prevents data races but not deadlocks. Always acquire locks in a consistent order, minimize lock scope, and prefer message passing (channels) over shared state when possible.
Key Takeaways
- ✅ Rust prevents data races at compile time through ownership and the type system
- ✅ Use channels (mpsc) for message-passing concurrency between threads
- ✅ Combine
Arc<T>andMutex<T>for shared mutable state across threads - ✅
SendandSynctraits are automatically derived by the compiler - ✅ Prefer channels over shared state when possible for simpler concurrent code