RwLock<T> (Read-Write Lock) ist eine spezialisierte Lock-Variante: er erlaubt beliebig viele gleichzeitige Reader, aber nur einen exklusiven Writer. Während ein Writer den Lock hält, müssen alle Reader warten; während Reader aktiv sind, muss ein Writer warten. Das passt zu read-heavy Workloads: Konfigurations-Werte, die selten ändern aber oft gelesen werden; eine Lookup-Tabelle, die mehrere Threads gleichzeitig konsultieren. Für diese Szenarien skaliert RwLock besser als Mutex, weil das Lesen parallel passiert. Für 50/50 Workloads oder schreib-lastige Szenarien ist Mutex meist schneller, weil RwLock-Operationen selbst etwas teurer sind.

Grundverwendung

RwLock<T> hat zwei Lock-Methoden: read() für Lese-Zugriff (geteilt), write() für Schreib-Zugriff (exklusiv). Beide geben Result<Guard, PoisonError> zurück.

Rust RwLock Basics
use std::sync::RwLock;

fn main() {
    let lock = RwLock::new(String::from("Hello"));

    // Mehrere Reader gleichzeitig OK
    {
        let r1 = lock.read().unwrap();
        let r2 = lock.read().unwrap();
        println!("{r1}, {r2}");
        // Beide leben gleichzeitig, kein Konflikt
    }

    // Writer — exklusiv
    {
        let mut w = lock.write().unwrap();
        w.push_str(" World");
    }

    // Lesen nach dem Write
    println!("{}", *lock.read().unwrap());   // "Hello World"
}

read() gibt einen RwLockReadGuard<T> zurück — wirkt per Deref wie &T. write() gibt einen RwLockWriteGuard<T> — wirkt per Deref/DerefMut wie &mut T. Beide sind RAII-Token: beim Drop wird der Lock automatisch freigegeben.

Wichtig: ein Writer-Lock blockiert auch andere Reader. Während ein Writer aktiv ist, kommt niemand dran. Und während Reader aktiv sind, muss ein wartender Writer warten.

Wann RwLock besser als Mutex ist

Die Faustregel: read-heavy Workloads (viele Reads, wenige Writes) profitieren von RwLock. Konkret heißt das:

  • Reader können parallel laufen — kein Serialisierungs-Engpass am Lock.
  • Writer haben dafür etwas mehr Overhead pro Operation (komplexere Synchronisation als bei Mutex).

Faustregel:

  • >90% Reads, <10% Writes: RwLock skaliert besser.
  • >50% Writes: Mutex ist meist schneller — Reader können sowieso nicht parallel laufen, also bringt RwLock nichts.
  • Sehr kurze kritische Sektionen: Mutex ist meist schneller, weil der zusätzliche RwLock-Overhead die Read-Parallelität nicht aufwiegt.

In Mikro-Benchmarks: ein einzelner read() ist etwa 1.5–3× langsamer als ein einzelner Mutex::lock(). Erst wenn mehrere Reader gleichzeitig aktiv sind, gewinnt RwLock — und das nur, wenn die kritischen Sektionen lang genug sind, dass die Parallelisierung lohnt.

Rust Read-heavy Pattern
use std::sync::{Arc, RwLock};
use std::thread;

fn main() {
    let config = Arc::new(RwLock::new(String::from("v1.0")));

    // 10 Reader-Threads, 1 Writer-Thread
    let mut handles = Vec::new();

    for i in 0..10 {
        let config = Arc::clone(&config);
        handles.push(thread::spawn(move || {
            for _ in 0..100 {
                let v = config.read().unwrap();
                // ... v lesen ...
                let _ = v.len();
            }
            println!("Reader {i} fertig");
        }));
    }

    let writer_config = Arc::clone(&config);
    handles.push(thread::spawn(move || {
        std::thread::sleep(std::time::Duration::from_millis(50));
        *writer_config.write().unwrap() = String::from("v1.1");
        println!("Config updated");
    }));

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

Die 10 Reader laufen weitgehend parallel — sie schließen sich gegenseitig nicht aus. Der eine Writer blockiert kurz; danach lesen alle wieder den neuen Wert.

Writer Starvation — eine Falle

In Konstellationen mit vielen Readern und wenigen Writern kann ein Writer "verhungern": ständig sind Reader aktiv, ein Writer wartet endlos.

Die Stdlib-RwLock ist (auf den meisten Plattformen) write-preferring: ein wartender Writer blockiert neue Reader, damit er irgendwann zum Zug kommt. Aber: das exakte Verhalten ist plattform-abhängig (Linux-pthread-rwlock kann anders sein als Windows-SRWLock).

Wenn dir das Fairness-Verhalten wichtig ist, schaue auf parking_lot::RwLock aus der parking_lot-Crate — sie hat konsistentere Garantien und ist oft schneller als die Stdlib-Version.

Lock-Upgrade und Downgrade — nicht direkt

Eine Frage, die aufkommt: kann ich von Read zu Write „upgraden"? Antwort: in der Stdlib-RwLock nein. Wenn du Schreibzugriff brauchst, musst du den Read-Guard erst droppen und dann write() aufrufen — was ein Race-Risiko schafft (andere Threads könnten zwischendurch schreiben).

Rust Kein direktes Upgrade
use std::sync::RwLock;

fn main() {
    let lock = RwLock::new(0);

    // FALSCH (geht nicht):
    // let r = lock.read().unwrap();
    // let w = lock.write().unwrap();   // Deadlock — wartet auf r
    // drop(r);

    // RICHTIG:
    {
        let r = lock.read().unwrap();
        let _ = *r;     // lesen
    }   // r gedroppt → Lock frei

    let mut w = lock.write().unwrap();
    *w += 1;
}

Die Crate parking_lot::RwLock hat einen upgradable_read()-Modus, der Upgrades ermöglicht. Wer das braucht, sollte direkt diese Crate nutzen.

Poisoning und try-Methoden

Wie Mutex hat auch RwLock Poisoning: wenn ein Writer-Thread mit gehaltenem Write-Lock panickt, wird der Lock poisoned. read() und write() geben dann Err(PoisonError) zurück.

Reader-Panic poisoned in der aktuellen Stdlib nicht den Lock (Stand 2026) — der Reader hat den Wert nicht modifiziert, also bleibt er konsistent.

Außerdem gibt es nicht-blockierende Varianten:

  • try_read() — gibt Err zurück, wenn ein Writer aktiv oder wartend ist.
  • try_write() — gibt Err zurück, wenn irgendein Lock aktiv ist (Reader oder Writer).
Rust try_read
use std::sync::RwLock;

fn main() {
    let lock = RwLock::new(42);
    let _w = lock.write().unwrap();

    match lock.try_read() {
        Ok(_) => println!("Read OK"),
        Err(_) => println!("Read blocked (writer active)"),
    }
    // Ausgabe: "Read blocked (writer active)"
}

Beispiel: Read-heavy Config

Klassische Anwendung: ein Konfigurations-Wert, der von vielen Threads gelesen wird, aber selten aktualisiert wird (z.B. nach SIGHUP, nach Admin-Aktion).

Rust Config mit RwLock
use std::sync::{Arc, RwLock};
use std::thread;
use std::time::Duration;

#[derive(Clone, Debug)]
struct Config {
    api_url: String,
    timeout_ms: u32,
}

fn main() {
    let config = Arc::new(RwLock::new(Config {
        api_url: String::from("https://api.v1.example.com"),
        timeout_ms: 5000,
    }));

    // Worker-Threads — viele Reader
    let mut handles = Vec::new();
    for i in 0..3 {
        let config = Arc::clone(&config);
        handles.push(thread::spawn(move || {
            for _ in 0..3 {
                let c = config.read().unwrap();
                println!("Worker {i}: {} (timeout {})", c.api_url, c.timeout_ms);
                drop(c);   // Read-Lock freigeben
                thread::sleep(Duration::from_millis(50));
            }
        }));
    }

    // Admin-Thread — seltene Writer
    let config_admin = Arc::clone(&config);
    handles.push(thread::spawn(move || {
        thread::sleep(Duration::from_millis(75));
        let mut c = config_admin.write().unwrap();
        c.api_url = String::from("https://api.v2.example.com");
        c.timeout_ms = 3000;
        println!("Admin: Config updated");
    }));

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

Die Worker lesen die Config sehr häufig. Wenn alle Mutex verwenden würden, würden sie sich gegenseitig serialisieren. Mit RwLock laufen sie parallel — nur der eine Admin-Update blockiert kurz alle.

Mutex vs. RwLock — die Entscheidungstabelle

SituationEmpfehlung
Mostly-Read mit langen Read-SektionenRwLock
Mostly-Read mit kurzen Read-SektionenMutex (Overhead von RwLock lohnt nicht)
Read und Write gleich häufigMutex (weniger Komplexität, oft schneller)
Hauptsächlich WriteMutex
Single-threaded mit Interior MutabilityRefCell (kein Lock-Overhead)
Single-Wert (Counter, Flag)AtomicXxx (lock-frei)

In der Praxis ist Mutex der bessere Default. RwLock lohnt sich erst, wenn du gemessen hast, dass Mutex zum Bottleneck wird und der Workload klar read-heavy ist.

Async-Variante: tokio::sync::RwLock

In Async-Code: nutze nicht std::sync::RwLock. Wie bei Mutex blockiert er den OS-Thread, was in Async katastrophal ist.

Stattdessen: tokio::sync::RwLock (für Tokio), async_std::sync::RwLock (für async-std). API ist analog, nur mit .await:

Rust Tokio-RwLock (Skizze)
// Erfordert: tokio = { version = "1", features = ["full"] }
use std::sync::Arc;
use tokio::sync::RwLock;

# async fn run() {
let config = Arc::new(RwLock::new(String::from("v1")));

let c2 = Arc::clone(&config);
let handle = tokio::spawn(async move {
    let value = c2.read().await;        // .await statt .unwrap()
    println!("{value}");
});

handle.await.unwrap();
# }

Wichtig: ein Tokio-RwLockReadGuard darf in einem Tokio-Task gehalten werden, der yieldet (z.B. über .await), ohne den ganzen Worker zu blockieren. Mehr Details im Async-Kapitel.

Praxis: RwLock im echten Code

Feature-Flag-Registry

Rust Feature-Flags
use std::sync::{Arc, RwLock};
use std::collections::HashMap;
use std::thread;

struct FeatureFlags {
    flags: RwLock<HashMap<String, bool>>,
}

impl FeatureFlags {
    fn new() -> Self {
        FeatureFlags { flags: RwLock::new(HashMap::new()) }
    }

    fn is_active(&self, name: &str) -> bool {
        self.flags.read().unwrap()
            .get(name).copied().unwrap_or(false)
    }

    fn enable(&self, name: impl Into<String>) {
        self.flags.write().unwrap().insert(name.into(), true);
    }

    fn disable(&self, name: impl Into<String>) {
        self.flags.write().unwrap().insert(name.into(), false);
    }
}

fn main() {
    let flags = Arc::new(FeatureFlags::new());
    flags.enable("new-ui");

    let mut handles = Vec::new();
    for i in 0..5 {
        let flags = Arc::clone(&flags);
        handles.push(thread::spawn(move || {
            if flags.is_active("new-ui") {
                println!("Worker {i}: showing new UI");
            } else {
                println!("Worker {i}: showing old UI");
            }
        }));
    }
    for h in handles { h.join().unwrap(); }
}

Feature-Flag-Service: viele Reads (jeder Request prüft Flags), selten Writes (Admin toggelt). Klassischer RwLock-Anwendungsfall.

Lazy-Cache mit Read-Heavy Pattern

Rust Cache
use std::sync::{Arc, RwLock};
use std::collections::HashMap;
use std::thread;

struct Cache {
    entries: RwLock<HashMap<String, String>>,
}

impl Cache {
    fn new() -> Self {
        Cache { entries: RwLock::new(HashMap::new()) }
    }

    fn lookup(&self, key: &str) -> Option<String> {
        self.entries.read().unwrap().get(key).cloned()
    }

    fn insert(&self, key: String, value: String) {
        self.entries.write().unwrap().insert(key, value);
    }
}

fn main() {
    let cache = Arc::new(Cache::new());
    cache.insert(String::from("a"), String::from("alpha"));
    cache.insert(String::from("b"), String::from("beta"));

    let mut handles = Vec::new();
    for i in 0..4 {
        let cache = Arc::clone(&cache);
        handles.push(thread::spawn(move || {
            let key = if i % 2 == 0 { "a" } else { "b" };
            if let Some(v) = cache.lookup(key) {
                println!("Thread {i}: {key} -> {v}");
            }
        }));
    }
    for h in handles { h.join().unwrap(); }
}

Cache mit viel Lookups, gelegentlich Inserts. RwLock erlaubt parallele Lookups — bei vielen Lese-Threads ein Performance-Gewinn gegenüber Mutex.

Routing-Tabelle

Rust Router
use std::sync::{Arc, RwLock};
use std::collections::HashMap;

struct Router {
    routes: RwLock<HashMap<String, String>>,
}

impl Router {
    fn new() -> Self {
        Router { routes: RwLock::new(HashMap::new()) }
    }

    fn route(&self, path: &str) -> Option<String> {
        self.routes.read().unwrap().get(path).cloned()
    }

    fn register(&self, path: impl Into<String>, target: impl Into<String>) {
        self.routes.write().unwrap().insert(path.into(), target.into());
    }
}

fn main() {
    let r = Router::new();
    r.register("/api/users", "service-a");
    r.register("/api/orders", "service-b");

    println!("{:?}", r.route("/api/users"));
    println!("{:?}", r.route("/api/orders"));
}

Router-Tabelle wird beim Start (oder Hot-Reload) befüllt, dann hauptsächlich gelesen. Typisches read-heavy Pattern.

Subscriber-Liste mit RwLock

Rust Subscriber-Liste
use std::sync::{Arc, RwLock};
use std::thread;

struct EventBus {
    subscribers: RwLock<Vec<String>>,
}

impl EventBus {
    fn new() -> Self {
        EventBus { subscribers: RwLock::new(Vec::new()) }
    }

    fn subscribe(&self, name: impl Into<String>) {
        self.subscribers.write().unwrap().push(name.into());
    }

    fn list_all(&self) -> Vec<String> {
        self.subscribers.read().unwrap().clone()
    }

    fn count(&self) -> usize {
        self.subscribers.read().unwrap().len()
    }
}

fn main() {
    let bus = Arc::new(EventBus::new());

    // Subscriber registrieren (Writer)
    for name in ["LoggerA", "LoggerB", "Metrics"] {
        bus.subscribe(name);
    }

    // Mehrere Threads lesen die Liste parallel
    let mut handles = Vec::new();
    for i in 0..3 {
        let bus = Arc::clone(&bus);
        handles.push(thread::spawn(move || {
            println!("Thread {i}: {} subscribers", bus.count());
        }));
    }
    for h in handles { h.join().unwrap(); }
}

Subscriber-Liste mit viel Lookups (jedes Event prüft alle Subscriber) und seltenen Subscribe/Unsubscribe-Operationen.

Hot-Reloadable Config

Rust Hot-Reload
use std::sync::{Arc, RwLock};
use std::thread;
use std::time::Duration;

#[derive(Clone)]
struct Config {
    log_level: String,
    max_retries: u32,
}

fn load_config() -> Config {
    Config {
        log_level: String::from("INFO"),
        max_retries: 3,
    }
}

fn reload_config() -> Config {
    Config {
        log_level: String::from("DEBUG"),
        max_retries: 5,
    }
}

fn main() {
    let config = Arc::new(RwLock::new(load_config()));

    // Worker-Threads lesen Config bei jedem Request
    let mut workers = Vec::new();
    for i in 0..3 {
        let config = Arc::clone(&config);
        workers.push(thread::spawn(move || {
            for _ in 0..3 {
                let c = config.read().unwrap();
                println!("Worker {i}: level={}, retries={}", c.log_level, c.max_retries);
                drop(c);
                thread::sleep(Duration::from_millis(50));
            }
        }));
    }

    // Reload-Thread simuliert Admin-Aktion
    let config_reload = Arc::clone(&config);
    workers.push(thread::spawn(move || {
        thread::sleep(Duration::from_millis(75));
        *config_reload.write().unwrap() = reload_config();
        println!("Config reloaded");
    }));

    for h in workers { h.join().unwrap(); }
}

Klassisches Pattern: Worker lesen Config bei jedem Request, Admin kann jederzeit eine neue Version laden. Mit RwLock blocken die Worker nur kurz beim Reload.

FAQ

Mehrere Reader gleichzeitig, ein Writer exklusiv.

RwLock erlaubt parallele Reads — der zentrale Vorteil gegenüber Mutex. Writer blockieren alle Reader und werden von allen Readern blockiert. Während eine Writer-Operation läuft, ist die kritische Sektion serialisiert.

Lohnt sich nur bei read-heavy Workloads.

Faustregel: >90% Reads, weniger Writes. Bei 50/50 ist Mutex oft schneller (RwLock-Overhead). Bei sehr kurzen kritischen Sektionen: Mutex bevorzugen.

Kein Upgrade von Read zu Write.

Stdlib-RwLock kann nicht direkt upgraden. Read-Lock erst droppen, dann write() — mit Race-Risiko. Für Upgrade-Pattern: parking_lot::RwLock mit upgradable_read().

Writer-Starvation möglich.

Bei sehr vielen Readern und einem Writer kann der Writer „verhungern". Stdlib-RwLock ist auf den meisten Plattformen write-preferring, aber das exakte Verhalten ist plattform-abhängig. parking_lot hat konsistentere Garantien.

Poisoning bei Writer-Panic.

Wenn ein Writer-Thread panickt, wird der Lock poisoned. read()/write() geben dann Err(PoisonError) zurück. Reader-Panic poisoned nicht — der Reader hat den Wert nicht verändert.

Standard-Form: Arc>.

Wie bei Mutex: Arc für Owner-Sharing zwischen Threads, RwLock für die Synchronisation. Pro Thread ein Arc-Klon vor move ||.

In Async-Code: tokio::sync::RwLock.

Stdlib-RwLock blockiert den OS-Thread — verboten in Async. Tokio hat eine Variante mit .await-Lock-Methoden, die saubere Yield-Semantik haben.

Default ist Mutex. RwLock nur bei Bedarf.

Erst Mutex nutzen, bei messbarem Read-Bottleneck zu RwLock wechseln. Premature Optimization in beide Richtungen ist unsinnig — der Performance-Unterschied ist nicht groß genug, um spekulativ zu wählen.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Smart Pointers

Zur Übersicht