Threads in Rust: Understanding Mutex and RwLock (2025)
A while ago, I was building a small exchange as a practice project and quickly ran into multi-threading. I decided to pick up Rust Atomics and Locks by Mara Bos — an amazing book to understand Rust concurrency.
This inspired me to write this post to share what I’ve learned, and hopefully making it a bit easier for anyone else who’s exploring Rust threads and data synchronization.
Spawning Threads in Rust
Threads in Rust are created using std::thread::spawn. Each thread executes independently and can share data using synchronization primitives like Mutex or RwLock.
Here's a minimal example:
use std::thread;
fn main() {
let handle = thread::spawn(|| {
println!("Hello from a separate thread!");
});
println!("Hello from the main thread!");
handle.join().unwrap();
}Key points:
thread::spawncreates a new threadhandle.join()waits for it to finish, preventing early exit
Sharing Data Across Threads
If multiple threads need access to the same data, Rust requires safe synchronization. You can't just hand out &mut references — that's unsafe in concurrent contexts. Instead, you wrap your data in synchronization primitives that enforce safe access.
Two common primitives are:
Mutex<T>— Mutual Exclusion lockRwLock<T>— Read-Write lock
Both are provided by the std::sync module.
Mutex: One-at-a-Time Access
A Mutex<T> guarantees exclusive access to the data. At any given moment, only one thread may hold the lock.
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..5 {
let counter = Arc::clone(&counter);
handles.push(thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
}));
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap());
}Key points:
- Arc (Atomic Reference Counting) is needed because threads require shared ownership of data
counter.lock()blocks until the lock is available- Only one writer is allowed at a time
RwLock: Multiple Readers, Single Writer
RwLock<T> improves on Mutex<T> when reads dominate writes. It allows multiple readers at the same time, but only one writer at a time (exclusive).
use std::sync::{Arc, RwLock};
use std::thread;
fn main() {
let data = Arc::new(RwLock::new(0));
let mut handles = vec![];
// Multiple readers
for _ in 0..3 {
let data = Arc::clone(&data);
handles.push(thread::spawn(move || {
let val = data.read().unwrap();
println!("Read: {}", *val);
}));
}
// One writer
{
let mut val = data.write().unwrap();
*val += 1;
}
for handle in handles {
handle.join().unwrap();
}
}Key points:
read()acquires a shared lock (many threads can read simultaneously)write()acquires an exclusive lock (blocks readers/writers until done)
Mutex vs. RwLock: When to Use Which?
Mutex<T>
- Read concurrency: Single reader
- Write concurrency: Single writer
- Overhead: Lower (simpler)
- Best for: Frequent writes
RwLock<T>
- Read concurrency: Multiple readers allowed simultaneously
- Write concurrency: Single writer (blocks readers & other writers)
- Overhead: Higher (managing reader/writer states)
- Best for: Read-heavy workloads
Rule of Thumb:
- Use
Mutex<T>when writes are common or simplicity matters - Use
RwLock<T>when reads dominate and lock contention is high
Benchmarking Mutex vs RwLock
Performance
RwLock<T>often outperformsMutex<T>for many concurrent readers- But with frequent writes,
Mutex<T>might be simpler and faster
What's Next?
Understanding these concurrency primitives opens up more advanced patterns:
- Channels for message passing between threads
- Atomic types for lock-free programming
- async/await for cooperative concurrency
For production systems, consider thread pools and async runtimes like Tokio.
Final Thoughts
Rust's type system and ownership model ensure memory safety, but concurrency requires explicit synchronization.
Mutex<T> is simple and robust for one-at-a-time access. RwLock<T> shines in read-heavy scenarios with occasional writes. Both integrate seamlessly with Rust threads, and understanding their trade-offs helps build safer and faster concurrent systems.
For a deeper dive, I would suggest anyone to read Rust Atomics and Locks by Mara Bos.
FAQs
1. Why can't I just share references between threads?
Rust prevents data races at compile time. Shared mutable references could lead to undefined behavior in concurrent contexts.
2. When should I use Arc vs Rc?
Use Arc for thread-safe reference counting, Rc for single-threaded scenarios only.
3. What happens if a thread panics while holding a lock?
The lock becomes "poisoned" — other threads will get an error when trying to acquire it, but can still access the data if needed.
4. Is RwLock always better for read-heavy workloads?
Not always. The overhead of managing reader/writer states can sometimes make Mutex faster, especially with low contention.
5. Should I use std::sync or tokio's async primitives?
For blocking operations and CPU-bound work, use std::sync. For I/O-bound async work, use tokio::sync.