Mutex<T> (Mutual Exclusion) ist das klassische Lock-Primitive für thread-sichere Mutation. Mehrere Threads dürfen denselben Wert teilen — aber zur Mutation muss jeder Thread den Mutex locken, was alle anderen blockiert. Wer fertig ist, lässt den Lock los; ein wartender Thread kommt dran. Auf der Rust-Seite ist die Eleganz: der Lock liefert einen MutexGuard<T>, der per Deref wie &mut T wirkt, beim Drop den Lock automatisch wieder freigibt, und (durch das Borrow-System) sicherstellt, dass du nach dem Drop keinen Zugriff mehr hast. Damit ist Rust-Mutex deutlich weniger fehleranfällig als C-mutex — vergessenes Unlock geht nicht.

Grundverwendung

Ein Mutex<T> wird mit Mutex::new(wert) erzeugt. Der lock-Aufruf gibt ein Result<MutexGuard<T>, PoisonError> zurück — meistens unwrappt man es.

Rust Mutex Basics
use std::sync::Mutex;

fn main() {
    let m = Mutex::new(0_i32);

    // Lock holen — exklusiver Zugriff
    {
        let mut guard = m.lock().unwrap();
        *guard += 1;
        // guard wird beim Scope-Ende gedroppt → Lock freigegeben
    }

    // Lock nochmal holen — diesmal lesend
    {
        let guard = m.lock().unwrap();
        println!("Wert: {}", *guard);
    }
}

m.lock() blockiert, bis der Mutex frei ist. Der zurückgegebene MutexGuard ist ein Smart Pointer — durch Deref greift *guard auf den inneren Wert zu, durch DerefMut kannst du ihn mutieren. Beim Drop des Guards (Verlassen des Scope) wird der Lock automatisch freigegeben.

Das unwrap() löst den PoisonError-Fall auf — dazu gleich mehr.

Mutex zwischen Threads — Arc + Mutex

Der eigentliche Sinn von Mutex zeigt sich erst zwischen Threads. Da Mutex<T> selbst nicht klonbar ist, kombiniert man es mit Arc:

Rust Arc + Mutex Standard-Pattern
use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let counter = Arc::new(Mutex::new(0));

    let mut handles = Vec::new();
    for _ in 0..10 {
        let counter = Arc::clone(&counter);
        let h = thread::spawn(move || {
            let mut value = counter.lock().unwrap();
            *value += 1;
        });
        handles.push(h);
    }

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

    println!("Counter: {}", *counter.lock().unwrap());   // 10
}

Das ist das Standard-Pattern für thread-sicheren mutable State:

  1. Arc<Mutex<T>>Arc für mehrere Owner, Mutex für exklusive Mutation.
  2. Pro Thread ein Arc::clone vor dem move ||.
  3. Im Thread: lock(), mutieren, Guard fallen lassen.
  4. Nach allen Threads: Aggregat lesen (auch hier lockend).

Das funktioniert für jeden mutable State zwischen Threads — Counter, Listen, HashMaps, eigene Structs. Wenn die geteilten Daten ein einfaches Numeric oder Bool sind, lohnt sich oft die Atomic-Variante (siehe Concurrency-Kapitel) — schneller, ohne Lock-Overhead.

MutexGuard — die Sicherheits-Mechanik

Der Trick, der Rust-Mutex sicher macht, ist der MutexGuard. Er ist ein RAII-Token:

  • Bei Erzeugung (lock()) wird der Lock geholt.
  • Solange er lebt, ist der Lock gehalten.
  • Beim Drop (Verlassen des Scopes) wird der Lock freigegeben.

Du kannst den Lock nicht „vergessen". Du kannst auch keine Referenz auf den inneren Wert über das Guard-Drop hinaus halten — der Borrow-Checker prüft das.

Rust Guard-Scope
use std::sync::Mutex;

fn main() {
    let m = Mutex::new(String::from("hello"));

    {
        let guard = m.lock().unwrap();
        println!("In lock: {guard}");
        // guard wird hier am Block-Ende gedroppt → Lock frei
    }

    // Wer den Lock jetzt nochmal will, bekommt ihn sofort
    let g2 = m.lock().unwrap();
    println!("After re-lock: {g2}");
}

Vergleich zu C: dort musst du explizit pthread_mutex_unlock(&mtx) aufrufen. Vergisst du es, ist der Lock für immer gehalten. In Rust unmöglich — der Compiler erzwingt Drop, Drop entlockt.

Poisoning — was passiert bei Panic im Lock?

Stell dir vor: ein Thread holt den Lock, modifiziert den inneren Wert teilweise, und panickt. Der Wert ist jetzt in einem inkonsistenten Zustand. Andere Threads, die später den Lock holen, könnten beschädigte Daten lesen.

Rust schützt davor mit Poisoning: panickt ein Thread, der den Lock hält, wird der Mutex als „vergiftet" markiert. Alle folgenden lock()-Aufrufe geben Err(PoisonError) zurück.

Rust Poison-Demo
use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let m = Arc::new(Mutex::new(0));
    let m2 = Arc::clone(&m);

    let handle = thread::spawn(move || {
        let mut g = m2.lock().unwrap();
        *g = 42;
        panic!("oh no!");    // → Mutex wird poisoned
    });

    // Thread wird abgebrochen
    let _ = handle.join();

    // Lock holen im Main: PoisonError
    match m.lock() {
        Ok(g) => println!("OK: {}", *g),
        Err(e) => {
            println!("Poisoned, but we still read the value: {}", *e.get_ref());
            // get_ref() / get_mut() / into_inner() liefern den Wert,
            // den der panickte Thread hinterlassen hat.
        }
    }
}

PoisonError ist kein Fehler, der „verschwindet" — der Mutex bleibt poisoned. Du kannst per into_inner()/get_ref()/get_mut() den Wert trotzdem extrahieren, wenn du weißt, dass das sicher ist.

Praktischer Umgang: in den meisten Anwendungen ist unwrap() genug — wenn ein Thread panickt, ist das Programm sowieso in einem schwer reparierbaren Zustand. In Server-Code mit Recovery-Logik nutzt man die PoisonError-Behandlung gezielter.

Deadlocks — die zentrale Falle

Ein Deadlock entsteht, wenn zwei Threads Locks in unterschiedlicher Reihenfolge holen. Klassisches Beispiel:

Rust Deadlock-Risiko
use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let a = Arc::new(Mutex::new(0));
    let b = Arc::new(Mutex::new(0));

    let a2 = Arc::clone(&a);
    let b2 = Arc::clone(&b);

    let t1 = thread::spawn(move || {
        let _la = a2.lock().unwrap();
        std::thread::sleep(std::time::Duration::from_millis(10));
        let _lb = b2.lock().unwrap();   // wartet auf b
    });

    let a3 = Arc::clone(&a);
    let b3 = Arc::clone(&b);
    let t2 = thread::spawn(move || {
        let _lb = b3.lock().unwrap();
        std::thread::sleep(std::time::Duration::from_millis(10));
        let _la = a3.lock().unwrap();   // wartet auf a
    });

    t1.join().unwrap();
    t2.join().unwrap();
    // → Deadlock möglich: Thread 1 hat a, will b. Thread 2 hat b, will a.
    //   Beide warten ewig.
}

Die klassische Lösung: konsistente Lock-Reihenfolge. Wenn alle Threads Locks immer in derselben Reihenfolge holen (z.B. immer erst a, dann b), kann kein Zyklus entstehen.

Pragmatisch: vermeide es, mehrere Locks gleichzeitig zu halten, wenn es geht. Wenn du Locks verschachteln musst, dokumentiere die Reihenfolge in einem Modul-Kommentar.

try_lock — nicht-blockierende Variante

Manchmal willst du nicht blockieren, sondern nur prüfen: ist der Mutex frei?

Rust try_lock
use std::sync::Mutex;

fn main() {
    let m = Mutex::new(0);

    match m.try_lock() {
        Ok(mut g) => {
            *g = 42;
            println!("Lock acquired, value set to {}", *g);
        }
        Err(_) => {
            println!("Lock busy, doing something else");
        }
    }
}

try_lock gibt Result<MutexGuard, TryLockError> zurück — Ok wenn der Lock frei war, Err wenn nicht. Nützlich für „opportunistisches" Locking, wo Blockieren teurer wäre als die Operation auszulassen.

Performance — was kostet ein Lock?

Ein nicht-blockierender Lock (kein anderer hält ihn) kostet einige Atomic-Operationen — wenige Nanosekunden bis ~100 ns. Ein blockierender Lock kostet einen Context-Switch, also Mikrosekunden bis Millisekunden, plus die Wartezeit auf den haltenden Thread.

Das heißt: in Code mit niedriger Lock-Contention (selten gleichzeitige Zugriffe) ist der Overhead klein. In Code mit hoher Contention (viele Threads kämpfen um denselben Lock) wird der Lock zum Bottleneck.

Optimierungs-Strategien:

  • Kleinere kritische Sektionen: hole den Lock nur für die Mutation, nicht für lange Berechnungen.
  • Lock-Aufteilung: statt einem großen Mutex viele kleine. Klassisch: pro Shard ein Lock statt einem globalen.
  • Lock-freie Datenstrukturen: std::sync::atomic für einzelne Werte, crossbeam-Crate für komplexere Strukturen.
  • RwLock statt Mutex: bei read-heavy Workloads. Mehr im nächsten Artikel.

Mutex und Async — nicht das Stdlib-Mutex nutzen!

Eine wichtige Falle: in Async-Code (Tokio, async-std) sollst du nicht std::sync::Mutex nutzen, sondern tokio::sync::Mutex (oder das Pendant deiner Runtime). Grund: std::sync::Mutex::lock() blockiert den OS-Thread. In Async ist das katastrophal — der Thread, der einen Future ausführt, blockiert, alle anderen Futures auf diesem Thread können nicht fortgesetzt werden.

tokio::sync::Mutex::lock() ist ein async Funktions-Aufruf — bei belegt-Lock yieldet er den Future, andere Futures auf dem Thread laufen weiter, der haltende Thread wird benachrichtigt, wenn der Lock frei wird.

Beispiel-Skizze (echter Async-Code im Async-Kapitel):

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

# async fn run() {
let m = Arc::new(Mutex::new(0));

let m2 = Arc::clone(&m);
let handle = tokio::spawn(async move {
    let mut g = m2.lock().await;     // .await statt .unwrap()
    *g += 1;
});

handle.await.unwrap();
# }

Faustregel: in synchronen Threads (std::thread) std::sync::Mutex. In Async-Tasks die Mutex deiner Runtime.

Praxis: Mutex im echten Code

Counter zwischen Worker-Threads

Rust Worker-Counter
use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let processed = Arc::new(Mutex::new(0_u32));
    let errors = Arc::new(Mutex::new(0_u32));

    let mut handles = Vec::new();
    for i in 0..5 {
        let processed = Arc::clone(&processed);
        let errors = Arc::clone(&errors);

        handles.push(thread::spawn(move || {
            for _ in 0..100 {
                // Simulation: 10% Fehler-Rate
                if i % 10 == 0 {
                    *errors.lock().unwrap() += 1;
                } else {
                    *processed.lock().unwrap() += 1;
                }
            }
        }));
    }

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

    println!("Processed: {}", *processed.lock().unwrap());
    println!("Errors:    {}", *errors.lock().unwrap());
}

Klassisches Pattern: jeder Worker-Thread inkrementiert geteilte Counter. Mutex schützt vor Race Conditions, Arc gibt mehrfache Owner. Bei einfachen Countern wäre AtomicU32 schneller — bei komplexerem State (Liste, HashMap) ist Mutex die richtige Wahl.

Geteilte Queue (Producer-Consumer-Idee)

Rust Queue
use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let queue = Arc::new(Mutex::new(Vec::<i32>::new()));

    // Producer
    let producer = {
        let queue = Arc::clone(&queue);
        thread::spawn(move || {
            for i in 0..5 {
                queue.lock().unwrap().push(i);
            }
        })
    };

    producer.join().unwrap();

    // Consumer (sequentiell nach Producer)
    let consumer = {
        let queue = Arc::clone(&queue);
        thread::spawn(move || {
            let mut q = queue.lock().unwrap();
            while let Some(item) = q.pop() {
                println!("Processed: {item}");
            }
        })
    };

    consumer.join().unwrap();
}

Eine triviale Queue mit Mutex. Für echte Producer-Consumer-Patterns ist std::sync::mpsc oder crossbeam::channel besser — sie sind speziell für diesen Anwendungsfall optimiert.

Logger mit Buffer

Rust Buffered-Logger
use std::sync::{Arc, Mutex};
use std::thread;

struct Logger {
    buffer: Mutex<Vec<String>>,
}

impl Logger {
    fn new() -> Self {
        Logger { buffer: Mutex::new(Vec::new()) }
    }

    fn log(&self, msg: impl Into<String>) {
        self.buffer.lock().unwrap().push(msg.into());
    }

    fn flush(&self) {
        let mut b = self.buffer.lock().unwrap();
        for line in b.drain(..) {
            println!("[LOG] {line}");
        }
    }
}

fn main() {
    let logger = Arc::new(Logger::new());

    let mut handles = Vec::new();
    for i in 0..3 {
        let logger = Arc::clone(&logger);
        handles.push(thread::spawn(move || {
            logger.log(format!("Thread {i} starting"));
            logger.log(format!("Thread {i} working"));
        }));
    }

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

Logger mit thread-safer Buffer. Jeder Thread fügt Log-Einträge hinzu, ein Flush-Aufruf gibt sie aus.

Cache mit Lock

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

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

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

    fn get_or_compute(&self, key: &str) -> String {
        {
            let map = self.entries.lock().unwrap();
            if let Some(v) = map.get(key) {
                return v.clone();
            }
        }
        // Lock vor der teuren Operation freigeben:
        let fresh = format!("data for {key}");

        self.entries.lock().unwrap().insert(key.to_string(), fresh.clone());
        fresh
    }
}

fn main() {
    let cache = Arc::new(Cache::new());
    let mut handles = Vec::new();
    for i in 0..3 {
        let cache = Arc::clone(&cache);
        handles.push(thread::spawn(move || {
            let key = format!("user-{}", i % 2);
            println!("{}: {}", key, cache.get_or_compute(&key));
        }));
    }
    for h in handles { h.join().unwrap(); }
}

Cache mit Lock — beachte das doppelte Lock-Pattern: erst kurz lockend prüfen, ob der Wert existiert. Wenn nicht: Lock freigeben (kein Lock während der teuren Berechnung), berechnen, wieder locken, einfügen. So bleibt die kritische Sektion kurz.

Counter mit Atomic-Alternative

Rust Atomic statt Mutex
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
use std::thread;

fn main() {
    // Für einfache Counter: AtomicU32 ist schneller als Mutex<u32>
    let counter = Arc::new(AtomicU32::new(0));

    let mut handles = Vec::new();
    for _ in 0..10 {
        let counter = Arc::clone(&counter);
        handles.push(thread::spawn(move || {
            for _ in 0..1000 {
                counter.fetch_add(1, Ordering::Relaxed);
            }
        }));
    }

    for h in handles { h.join().unwrap(); }
    assert_eq!(counter.load(Ordering::Relaxed), 10_000);
}

Bei einfachen Primitivtypen ist Atomic die effizientere Wahl. Lock-freie Operation, kein Context-Switch, deutlich schneller als Mutex.

Pool mit Mutex (Singleton-Style)

Rust Connection-Pool (Skizze)
use std::sync::{Arc, Mutex};
use std::thread;

struct Connection {
    id: u32,
}

struct Pool {
    connections: Mutex<Vec<Connection>>,
}

impl Pool {
    fn new(size: u32) -> Self {
        let conns = (0..size).map(|id| Connection { id }).collect();
        Pool { connections: Mutex::new(conns) }
    }

    fn take(&self) -> Option<Connection> {
        self.connections.lock().unwrap().pop()
    }

    fn give_back(&self, c: Connection) {
        self.connections.lock().unwrap().push(c);
    }
}

fn main() {
    let pool = Arc::new(Pool::new(3));

    let mut handles = Vec::new();
    for i in 0..5 {
        let pool = Arc::clone(&pool);
        handles.push(thread::spawn(move || {
            if let Some(c) = pool.take() {
                println!("Thread {i}: using Connection {}", c.id);
                pool.give_back(c);
            } else {
                println!("Thread {i}: no pool slot free");
            }
        }));
    }
    for h in handles { h.join().unwrap(); }
}

Connection-Pool mit Mutex. Jeder Thread holt eine Connection, nutzt sie, gibt sie zurück. Echte Pools nutzen meist eine spezialisierte Crate wie r2d2 oder bb8 — das Pattern ist aber identisch.

Besonderheiten

Mutex = Mutual Exclusion: ein Thread, exklusiver Zugriff.

Andere Threads blockieren am lock()-Aufruf, bis der Lock frei wird. Rust's MutexGuard ist RAII: Lock wird automatisch beim Scope-Ende freigegeben — vergessenes Unlock ist unmöglich.

Standard-Pattern für mutable Shared State: Arc>.

Arc fürs Owner-Sharing zwischen Threads, Mutex für die exklusive Mutation. Pro Thread ein Arc-Klon, der in move || rein wandert.

Poisoning: Panic im Lock = Mutex vergiftet.

lock() gibt danach PoisonError. Schutz gegen inkonsistente Daten. Praktisch oft mit unwrap() ignoriert; bei Recovery-Code gezielter behandelt (über get_ref/into_inner).

Deadlocks vermeiden: konsistente Lock-Reihenfolge.

Wenn mehrere Locks gleichzeitig gehalten werden müssen, immer in derselben Reihenfolge (z.B. „erst a, dann b"). Sonst potentielles Deadlock zwischen Threads, die in verschiedener Reihenfolge locken.

try_lock für nicht-blockierende Versuche.

Gibt Ok(guard) zurück, wenn der Lock frei war, sonst Err(WouldBlock). Nützlich für opportunistisches Locking.

In Async-Code: NICHT std::sync::Mutex.

Stdlib-Mutex blockiert den OS-Thread — katastrophal in Async-Runtimes. Stattdessen tokio::sync::Mutex (oder Pendant der Runtime), lock().await statt lock().unwrap().

Kritische Sektionen kurz halten.

Lock holen, mutieren, freigeben. Teure Operationen (Allocations, I/O) möglichst außerhalb des Locks. So bleibt Contention niedrig und die Performance gut.

Für einfache Werte: Atomics statt Mutex.

AtomicU32, AtomicBool etc. aus std::sync::atomic sind lock-frei. Für Counter, Flags, einfache Zähler deutlich schneller als Mutex. Mehr im Concurrency-Kapitel.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Smart Pointers

Zur Übersicht