---
title: How to Use RwLock<T> in Rust
summary: Use `RwLock<T>` when you need to share data across multiple threads where reads are frequent but writes are rare, as it allows unlimited concurrent readers or a single exclusive writer.
lang: en
slug: how-to-use-rwlockt-in-rust
published: 2026-04-17T16:46:41+00:00
updated: 2026-04-25T22:00:13+00:00
canonical: https://www.rustfaq.org/en/how-to-use-rwlockt-in-rust/
---

# How to Use RwLock<T> in Rust



## When reads dominate writes

You're building a leaderboard service. Thousands of players fetch their rank every second. The rank only changes when a match ends, which happens once a minute. If you wrap the leaderboard in a `Mutex`, every rank request has to wait in line behind every other request. The CPU sits idle while threads queue up for a lock that doesn't actually need to be exclusive. Reading the rank doesn't change the data, so blocking readers makes no sense.

`RwLock<T>` solves this. It allows any number of threads to read the data simultaneously. Writers still get exclusive access, but they only block when someone is actually writing. If reads vastly outnumber writes, `RwLock<T>` can dramatically increase throughput compared to a `Mutex`.

## The whiteboard rule

Think of a whiteboard in a busy office. Anyone can walk by and read what's written on it. Five people can stare at the board at the same time without interfering. But if someone grabs a marker to write, they need exclusive access. Everyone else has to wait until the writing is done.

`RwLock<T>` works the same way. It tracks two pieces of state: the number of active readers and whether a writer is active. When you call `.read()`, the lock increments the reader counter and returns a guard. Multiple guards can exist at once. When you call `.write()`, the lock waits until the reader counter is zero and no other writer is active. It then sets a writer flag and returns a mutable guard.

The guard object is the key to safety. The guard holds a reference to the lock. When the guard is dropped, it decrements the reader counter or clears the writer flag. This RAII pattern ensures the lock is always released, even if a function returns early or panics. You never call `unlock()` manually.

## Minimal example

Here is the basic pattern. You need `Arc` to share ownership across threads, and `RwLock` to protect the data inside.

```rust
use std::sync::{Arc, RwLock};
use std::thread;

fn main() {
    // Arc shares ownership. RwLock protects the data.
    // Convention: use Arc::clone(&var) to make it clear you're cloning the Arc, not the data.
    let data = Arc::new(RwLock::new(String::from("Hello")));

    let mut handles = vec![];

    // Spawn readers. They can all hold the lock at once.
    for i in 0..3 {
        let data_clone = Arc::clone(&data);
        handles.push(thread::spawn(move || {
            // .read() returns a Result. Unwrap for examples, handle errors in prod.
            // The guard holds the read lock until it drops.
            let guard = data_clone.read().unwrap();
            println!("Reader {} sees: {}", i, *guard);
        }));
    }

    // Spawn a writer. It blocks until all readers finish.
    let data_clone = Arc::clone(&data);
    handles.push(thread::spawn(move || {
        // .write() returns a mutable guard. Only one writer allowed.
        let mut guard = data_clone.write().unwrap();
        *guard = String::from("Updated");
        println!("Writer updated the value.");
    }));

    for h in handles {
        h.join().unwrap();
    }
}
```

## How the lock tracks state

The internal implementation maintains a reader counter and a writer flag. When a thread calls `.read()`, the lock checks if the writer flag is set. If a writer is active, the thread blocks. If not, the counter increments and the guard returns.

When a thread calls `.write()`, the lock waits until the counter is zero and the writer flag is clear. It then sets the flag and returns the guard. While the flag is set, new readers block. Existing readers must finish before the writer can proceed.

This design means readers never block other readers. Writers block readers and other writers. Readers block writers. The asymmetry is the whole point. If your workload is read-heavy, readers proceed without contention. Writers pay the cost of waiting, but that cost is amortized across the many reads that happen without waiting.

Scope your guards tightly. Holding a lock longer than necessary kills concurrency. If you need a value from the lock, copy it out and drop the guard immediately. The lock should protect the data, not the work.

## Realistic example: Config cache

A common use case is a configuration cache. The config is read by every request but updated rarely. Here is a pattern that scopes the guard correctly.

```rust
use std::sync::{Arc, RwLock};
use std::thread;
use std::time::Duration;

struct Config {
    max_connections: u32,
    debug_mode: bool,
}

fn main() {
    let config = Arc::new(RwLock::new(Config {
        max_connections: 100,
        debug_mode: false,
    }));

    let mut workers = vec![];

    // Workers read config frequently.
    for id in 0..5 {
        let config_clone = Arc::clone(&config);
        workers.push(thread::spawn(move || {
            for _ in 0..10 {
                // Scope the guard to a block.
                // This ensures the lock is released before any heavy work.
                let should_debug = {
                    let cfg = config_clone.read().unwrap();
                    cfg.debug_mode
                };
                // Guard drops here. Lock is free.

                if should_debug {
                    println!("Worker {} running in debug mode.", id);
                }
                // Simulate work without holding the lock.
                thread::sleep(Duration::from_millis(10));
            }
        }));
    }

    // Admin updates config rarely.
    let config_clone = Arc::clone(&config);
    workers.push(thread::spawn(move || {
        thread::sleep(Duration::from_millis(50));
        // Write lock blocks until all workers finish their current reads.
        let mut cfg = config_clone.write().unwrap();
        cfg.max_connections = 200;
        cfg.debug_mode = true;
        println!("Config updated.");
    }));

    for w in workers {
        w.join().unwrap();
    }
}
```

Notice the block around the read guard in the workers. The guard is created, the value is extracted, and the guard drops at the closing brace. The lock is released before the `println` and the sleep. If you held the guard across the sleep, all writes would block for the duration of the sleep. That turns a fast read lock into a bottleneck.

## Pitfalls and error handling

`RwLock<T>` has specific failure modes you need to handle.

### Poisoning

If a thread panics while holding a lock, the lock becomes poisoned. The next call to `.read()` or `.write()` returns `Err`. This prevents you from reading corrupted state. In production, you usually want to recover.

Calling `.unwrap()` on a poisoned lock will panic your thread too. Use `.into_inner()` on the error to recover the guard, or use `unwrap_or_else`.

```rust
let guard = data.read().unwrap_or_else(|e| e.into_inner());
```

This pattern recovers the lock even if a previous thread panicked. Treat the poison as a signal that something went wrong, but don't let it take down your service unless the data is truly unrecoverable.

### Writer starvation

Writer starvation happens when readers keep arriving faster than they leave. The writer waits forever because the reader counter never hits zero. `std::sync::RwLock` prioritizes readers. If you have a read-heavy workload with bursts of writes, writers might starve.

If starvation becomes a problem, switch to `Mutex<T>`. A `Mutex` is fairer in high-contention scenarios. Or re-architect to reduce shared state. Measure first. Starvation is rare in typical read-heavy workloads, but it can appear under load spikes.

### No lock upgrade

You cannot upgrade a read lock to a write lock. If you need to read, check a condition, and then write, you must release the read lock and acquire a write lock. This creates a race condition window where another thread can modify the data between the read and the write.

Design your logic to avoid this pattern. Batch updates. Or use a `Mutex` if you need atomic check-and-update semantics.

### Overhead

`RwLock<T>` has more overhead than `Mutex<T>`. It tracks a counter and manages fairness. If writes are frequent, or if reads are short and writes are rare but contention is low, a `Mutex` might be faster. The counter operations add cost. Profile your code. Don't reach for `RwLock` just because it sounds faster.

If you try to share a `RwLock` across threads without `Arc`, the compiler rejects you with `E0277` (trait bound not satisfied). `RwLock<T>` implements `Sync` if `T` implements `Send`. This allows shared access, but you still need `Arc` to clone the reference.

## Decision matrix

Use `RwLock<T>` when reads vastly outnumber writes and you need interior mutability across threads. Use `RwLock<T>` when readers can proceed concurrently without interfering, and writes are infrequent enough that writer blocking is acceptable. Use `Mutex<T>` when writes are frequent, or when the overhead of tracking reader counts outweighs the benefit of concurrent reads. Use `Mutex<T>` when you need fair locking under high contention to prevent starvation. Use `AtomicUsize` or `AtomicBool` when you only need to update a single primitive value and don't need complex state. Use channels when threads should communicate by sending messages rather than sharing mutable state. Use `tokio::sync::RwLock` when you are in an async runtime and need non-blocking lock acquisition.

Measure before you optimize. `RwLock` has more overhead than `Mutex` for simple cases. Scope your guards tightly. Holding a lock longer than necessary kills concurrency. Handle the poison. A panic in one thread shouldn't freeze your whole application.

## Where to go next

- [How to Share Data Between Threads in Rust](/en/how-to-share-data-between-threads-in-rust/)
- [How to Use Channels (mpsc) for Thread Communication in Rust](/en/how-to-use-channels-mpsc-for-thread-communication-in-rust/)
- [Thread Safety in Rust vs C++ and Java](/en/thread-safety-in-rust-vs-c-and-java/)