RwLock<T> in Rust: vs Mutex & Writer Starvation

Max WellsMax WellsFounder of Rustify

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 than Mutex<T> when reads are frequent and writes are rare. Like Mutex, it's combined with Arc<T> for cross-thread sharing: Arc<RwLock<T>>. The cost: more complex (writer starvation risk), and on some platforms RwLock can actually be slower than Mutex for 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 forWrite-heavy or balancedRead-heavy workloads
Deadlock riskLowerHigher (writer starvation)
PerformancePredictablePlatform-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 coming

The 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


  • 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::RwLock in async contexts to avoid blocking the executor
  • Send/Sync: RwLock<T> is Send + Sync when T: Send + Sync

Keep Reading

Ready to Land a $120k+ Rust Job in the US or Europe?