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::spawn creates a new thread
  • handle.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 lock
  • RwLock<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 outperforms Mutex<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.