TL;DR:
RwLock<T>(read-write lock) allows multiple threads to read simultaneously, but only one thread to write at a time. This is more efficient thanMutex<T>when reads are frequent and writes are rare. LikeMutex, it's combined withArc<T>for cross-thread sharing:Arc<RwLock<T>>. The cost: more complex (writer starvation risk), and on some platformsRwLockcan actually be slower thanMutexfor write-heavy workloads.
What Is RwLock<T>?
RwLock<T> tracks two types of borrows: shared reads (many at once via .read()) and exclusive writes (one at a time via .write()). A writer blocks until all readers finish, and readers block while a writer holds the lock.
use std::sync::{Arc, RwLock};
use std::thread;
fn main() {
let data = Arc::new(RwLock::new(vec![1, 2, 3]));
let mut handles = vec![];
// Spawn 3 reader threads; all can run simultaneously
for i in 0..3 {
let data = Arc::clone(&data);
handles.push(thread::spawn(move || {
let r = data.read().unwrap(); // shared read lock
println!("reader {i}: {:?}", *r);
}));
}
// Spawn 1 writer thread; waits for all readers to finish
{
let data = Arc::clone(&data);
handles.push(thread::spawn(move || {
let mut w = data.write().unwrap(); // exclusive write lock
w.push(4);
println!("writer: pushed 4");
}));
}
for h in handles { h.join().unwrap(); }
println!("final: {:?}", *data.read().unwrap());
}How Is RwLock Different From Mutex?
Mutex gives exclusive access to one thread at a time regardless of read/write intent. RwLock distinguishes reads from writes; concurrent reads are safe and allowed simultaneously.
Mutex<T> | RwLock<T> | |
|---|---|---|
| Concurrent readers | ❌ one thread at a time | ✅ unlimited |
| Concurrent writers | ❌ | ❌ |
| Locking API | .lock() | .read() / .write() |
| Best for | Write-heavy or balanced | Read-heavy workloads |
| Deadlock risk | Lower | Higher (writer starvation) |
| Performance | Predictable | Platform-dependent |
// Choose Mutex when writes are frequent
let counter = Arc::new(Mutex::new(0u64));
// Choose RwLock when reads dominate (e.g., shared config/cache)
let config = Arc::new(RwLock::new(AppConfig::default()));What Is Writer Starvation?
Writer starvation occurs when a steady stream of readers keeps arriving, preventing the writer from ever acquiring the exclusive lock. Some RwLock implementations prioritize writers to prevent this.
use std::sync::{Arc, RwLock};
let lock = Arc::new(RwLock::new(0));
// If readers constantly hold the lock:
let _r1 = lock.read().unwrap();
let _r2 = lock.read().unwrap();
// A write lock cannot be acquired until _r1 and _r2 are dropped
// On some systems, writers may wait indefinitely if readers keep comingThe parking_lot::RwLock (from the parking_lot crate) uses a fair policy that prevents starvation and is generally faster than std::sync::RwLock.
How Do You Use RwLock in Async Code?
Use tokio::sync::RwLock in async code; std::sync::RwLock blocks the thread while waiting, which stalls the async executor.
use tokio::sync::RwLock;
use std::sync::Arc;
#[tokio::main]
async fn main() {
let data = Arc::new(RwLock::new(vec![1, 2, 3]));
// Async read; suspends the task, not the thread
let r = data.read().await;
println!("{:?}", *r);
drop(r);
// Async write
let mut w = data.write().await;
w.push(4);
}Never hold a tokio::sync::RwLock guard across a blocking operation. Like Mutex, drop the guard before .await where possible.
When Should You Use RwLock?
Use RwLock for shared state that is read much more often than written; application config, caches, routing tables, feature flags.
use std::sync::{Arc, RwLock};
use std::collections::HashMap;
// Classic RwLock use case: shared cache
struct Cache {
inner: RwLock<HashMap<String, String>>,
}
impl Cache {
fn get(&self, key: &str) -> Option<String> {
self.inner.read().unwrap().get(key).cloned()
}
fn set(&self, key: String, value: String) {
self.inner.write().unwrap().insert(key, value);
}
}Frequently Asked Questions
Not guaranteed. On some platforms (especially Windows), std::sync::RwLock can be slower than Mutex due to implementation overhead. Always benchmark if performance matters. parking_lot::RwLock is consistently faster on all platforms.
Deadlock; the thread already holds the exclusive write lock and will wait forever for a read lock that can never be granted. Don't hold any RwLock guard while trying to acquire another guard on the same lock.
std::sync::RwLock doesn't support lock upgrading. You must drop the read lock first, then acquire the write lock. The parking_lot crate offers upgradable_read() which can be upgraded without fully releasing.
If your data is read-heavy (reads >> writes): Arc<RwLock<T>>. If writes are frequent or the critical section is very short: Arc<Mutex<T>>; simpler and predictably fast.
Sources
- std::sync::RwLock: Standard library docs
- parking_lot::RwLock: Faster alternative
- tokio::sync::RwLock: Async version
Related Glossary Terms
- Mutex: Simpler alternative; one lock for all access patterns
- Arc:
Arc<RwLock<T>>is the standard cross-thread shared state pattern - Async/Await: Use
tokio::sync::RwLockin async contexts to avoid blocking the executor - Send/Sync:
RwLock<T>isSend + SyncwhenT: Send + Sync
Keep Reading
- Rust Ownership and Borrowing Explained: RwLock extends ownership semantics to concurrent readers
- Rust Memory Safety: NSA and CISA: safe concurrent access without data races
- Rust in the Linux Kernel: read-write locks in kernel subsystems
