Mehrere Owner für denselben Wert haben? Rc für single-threaded, Arc für threads. Mutation hinter geteilten Referenzen? RefCell für single-threaded, Mutex für threads. Wenn du beides gleichzeitig brauchst — geteiltes Eigentum und Mutation — kombinierst du die Werkzeuge: Rc<RefCell<T>> oder Arc<Mutex<T>> (alternativ Arc<RwLock<T>> bei read-heavy). Diese Compound-Patterns sind so verbreitet, dass sie eigene Namen verdienen. Sie sind aber auch ein Code-Smell-Indikator: wenn dein Programm überall mit Rc<RefCell<T>> arbeitet, hat oft das Datenmodell ein Problem.
Was ist Rc<RefCell<T>>?
Eine Rc<RefCell<T>> ist genau das, was der Name sagt: ein Rc-Wrapper um einen RefCell-Wrapper um einen Wert. Konzeptuell:
Rcsorgt für geteiltes Eigentum: mehrere Stellen halten dieselbe Datenstruktur, die letzte räumt auf.RefCellsorgt für Interior Mutability: alle Owner haben nur geteilte Referenzen auf dasRefCell, dürfen aber per.borrow_mut()mutieren.
use std::rc::Rc;
use std::cell::RefCell;
fn main() {
let shared: Rc<RefCell<Vec<i32>>> = Rc::new(RefCell::new(Vec::new()));
let a = Rc::clone(&shared);
let b = Rc::clone(&shared);
a.borrow_mut().push(1);
b.borrow_mut().push(2);
shared.borrow_mut().push(3);
assert_eq!(*shared.borrow(), vec![1, 2, 3]);
assert_eq!(Rc::strong_count(&shared), 3);
}Alle drei (a, b, shared) zeigen auf dasselbe RefCell<Vec<i32>>. Jede der drei Bindungen kann .borrow() oder .borrow_mut() aufrufen, und alle sehen den aktualisierten Wert.
Die Verantwortlichkeits-Aufteilung
Wichtig zu verstehen: Rc und RefCell haben getrennte Verantwortlichkeiten.
Rc::clone(&shared)erhöht den Owner-Counter — ändert nichts am Inhalt.shared.borrow()/shared.borrow_mut()regelt den Zugriff auf den Inhalt — ändert nichts am Counter.
Die XOR-Regel (entweder Reader oder Writer, nicht beides) gilt für den RefCell-Teil, nicht für Rc. Du kannst beliebig viele Rc-Klone haben — die nehmen dem RefCell nichts weg. Nur die borrow/borrow_mut-Aufrufe konkurrieren miteinander.
use std::rc::Rc;
use std::cell::RefCell;
fn main() {
let shared = Rc::new(RefCell::new(0));
// 5 Rc-Klone — kein Problem
let _clones: Vec<Rc<RefCell<i32>>> = (0..5).map(|_| Rc::clone(&shared)).collect();
// Aber: zwei gleichzeitige mutable Borrows panicken
let _r1 = shared.borrow();
// let _r2 = shared.borrow_mut(); // → PANIC: already borrowed
let _ = _r1;
}Wann brauchst du das Pattern?
Faustregel: wenn mehrere Stellen denselben Wert besitzen sollen und ihn mutieren wollen, ist das Compound-Pattern die Antwort. Klassische Use-Cases:
- Graph-Strukturen: ein Knoten wird von mehreren Stellen referenziert, alle wollen ihn modifizieren können (Kanten hinzufügen etc.).
- Doubly-Linked-List: jeder Knoten zeigt auf prev und next, beide wollen modifiziert werden können.
- Observer/Subscription-Listen: mehrere Stellen halten den Subject, das Subject mutiert intern (Subscriber-Liste, Event-Queue).
- Caches mit Lazy-Loading: mehrere Konsumenten teilen denselben Cache, der Cache mutiert intern beim Befüllen.
Wenn du nur einen Owner brauchst und einfach &self-Methoden mit interner Mutation: RefCell allein reicht. Wenn du nur lesen willst und mehrere Owner: Rc allein reicht. Das Compound brauchst du wirklich nur bei der Schnittmenge.
Praxis: Doubly-Linked-List
use std::rc::{Rc, Weak};
use std::cell::RefCell;
struct Node {
value: i32,
prev: RefCell<Weak<Node>>,
next: RefCell<Option<Rc<Node>>>,
}
struct DLL {
head: Option<Rc<Node>>,
tail: Option<Rc<Node>>,
}
impl DLL {
fn new() -> Self {
DLL { head: None, tail: None }
}
fn append(&mut self, value: i32) {
let new_node = Rc::new(Node {
value,
prev: RefCell::new(Weak::new()),
next: RefCell::new(None),
});
match self.tail.take() {
Some(old_tail) => {
*new_node.prev.borrow_mut() = Rc::downgrade(&old_tail);
*old_tail.next.borrow_mut() = Some(Rc::clone(&new_node));
self.tail = Some(new_node);
}
None => {
self.head = Some(Rc::clone(&new_node));
self.tail = Some(new_node);
}
}
}
fn print(&self) {
let mut cursor = self.head.clone();
while let Some(n) = cursor {
print!("{} ", n.value);
cursor = n.next.borrow().clone();
}
println!();
}
}
fn main() {
let mut dll = DLL::new();
dll.append(1);
dll.append(2);
dll.append(3);
dll.print(); // 1 2 3
}Klassisches DLL-Beispiel. Jeder Knoten hat:
nextalsRc<Node>(owned, vorwärts).prevalsWeak<Node>(back-link, vermeidet Reference-Cycle).- Beide hinter
RefCell, damit man sie nach der Erzeugung noch ändern kann.
In der Praxis ist eine DLL in Rust komplex genug, dass viele zur Crate std::collections::LinkedList oder zur unsafe-Variante greifen. Aber das Pattern ist hier sichtbar.
Praxis: Observer-Pattern
use std::rc::{Rc, Weak};
use std::cell::RefCell;
trait Observer {
fn notify(&self, msg: &str);
}
struct Subject {
observers: RefCell<Vec<Weak<dyn Observer>>>,
}
impl Subject {
fn new() -> Rc<Self> {
Rc::new(Subject { observers: RefCell::new(Vec::new()) })
}
fn subscribe(&self, observer: Weak<dyn Observer>) {
self.observers.borrow_mut().push(observer);
}
fn send(&self, msg: &str) {
// Tote Observer entfernen, lebendige benachrichtigen
self.observers.borrow_mut().retain(|w| {
if let Some(o) = w.upgrade() {
o.notify(msg);
true
} else {
false
}
});
}
}
struct Logger { name: String }
impl Observer for Logger {
fn notify(&self, msg: &str) {
println!("Logger '{}': {msg}", self.name);
}
}
fn main() {
let subject = Subject::new();
let logger_a: Rc<dyn Observer> = Rc::new(Logger { name: String::from("A") });
let logger_b: Rc<dyn Observer> = Rc::new(Logger { name: String::from("B") });
subject.subscribe(Rc::downgrade(&logger_a));
subject.subscribe(Rc::downgrade(&logger_b));
subject.send("Event 1");
// Beide Logger empfangen.
drop(logger_a);
subject.send("Event 2");
// Nur Logger B empfängt (A wurde dropped, Weak::upgrade gibt None).
}Subject hält schwache Referenzen auf die Observer. Wenn ein Observer dropped, sieht der Subject das beim nächsten Versand und räumt auf. Das ist die robuste Form des Observer-Patterns — kein manuelles unregister() nötig.
Arc<Mutex<T>> — die Thread-Variante
Sobald Threads ins Spiel kommen, wechselst du Rc<RefCell<T>> zu Arc<Mutex<T>>. Mechanisch identisch, nur thread-sicher:
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let shared: Arc<Mutex<Vec<i32>>> = Arc::new(Mutex::new(Vec::new()));
let mut handles = Vec::new();
for i in 0..3 {
let shared = Arc::clone(&shared);
handles.push(thread::spawn(move || {
let mut data = shared.lock().unwrap();
data.push(i);
}));
}
for h in handles { h.join().unwrap(); }
let result = shared.lock().unwrap();
println!("{result:?}");
// [0, 1, 2] in irgendeiner Reihenfolge
}Das ist der Standard-Compound für threaded mutable shared state. Vergleich:
| Pattern | Wann | Single/Multi-thread |
|---|---|---|
Rc<RefCell<T>> | shared + mutable | Single |
Arc<Mutex<T>> | shared + mutable | Multi |
Arc<RwLock<T>> | shared + read-heavy mutable | Multi |
Der Wechsel ist syntaktisch ein Text-Replace, semantisch aber Thread-Sicherheit. Klassisches Vorgehen: erst mit Rc<RefCell> prototypen, beim Bedarf an Threads zu Arc<Mutex> wechseln.
Typische Stolperfallen
Doppel-Borrow im selben Thread
RefCell panickt bei XOR-Verletzungen:
use std::rc::Rc;
use std::cell::RefCell;
fn main() {
let r = Rc::new(RefCell::new(vec![1, 2, 3]));
let g = r.borrow();
// r.borrow_mut().push(4); // → PANIC
let _ = g;
}Klassisch in rekursiven Operationen, in denen man im Body einer Methode dieselbe Datenstruktur nochmal borrowt. Lösung: kurze Borrow-Scopes, vorher fertig lesen, dann mutieren.
Verschachtelte Locks im Mutex-Fall (Deadlock)
Bei Arc<Mutex<T>> ist die Falle ein Deadlock zwischen zwei Threads mit unterschiedlicher Lock-Reihenfolge. Siehe Mutex-Artikel.
Vergessen, dass Rc<RefCell> nicht thread-safe ist
Eine subtile Falle: man fängt mit Rc<RefCell<T>> an und entscheidet später, dass man Threads braucht. Wenn man die Suchen/Ersetzen-Migration zu Arc<Mutex<T>> macht, muss man alle borrow/borrow_mut zu lock().unwrap() umschreiben. Idiom ist: in einem klar abgegrenzten Modul den Smart-Pointer-Typ als Alias zu führen:
// type Shared<T> = std::rc::Rc<std::cell::RefCell<T>>;
// … Code nutzt Shared<T> überall …
// Wechsel zu Threads später:
// type Shared<T> = std::sync::Arc<std::sync::Mutex<T>>;
// (plus Anpassung der Borrow-Aufrufe — die API-Form ist verschieden)Pragmatisch reicht in vielen Fällen das direkte Type-Replace, gefolgt von Anpassungen, die der Compiler exakt zeigt.
Wann ist Rc<RefCell> ein Code-Smell?
Das Compound-Pattern ist mächtig, aber auch ein Indikator. Wenn dein Code überall Rc<RefCell<T>> hat, läuft etwas schief. Mögliche Anzeichen:
- Aliasing-Problem: du modellierst etwas, was Rust dir am liebsten verbieten würde, weil es schlechtes Design ist. Beispiel: wechselseitige Modifikation von Datenstrukturen, die eigentlich einen klaren Hauptverantwortlichen haben sollten.
- Falsches Owner-Modell: vielleicht sollte der Wert einen klaren Owner haben und alle anderen nur via
&referenzieren. Dann reicht Borrowing. - Übersetzungs-Übung aus OOP: aus Java/C++ kommend übersetzt man „Pointer auf Object" automatisch in
Rc<RefCell<T>>. Rust-idiomatisch sind oft Patterns mit Indices in Arenas, mit Channels, oder mit klarem Ownership.
Faustregel: nutze Rc<RefCell> gezielt, wenn ein Datenmodell es wirklich braucht. Ein paar Stellen sind OK, durchgehende Verwendung ist es nicht.
Praxis: weitere Anwendungen
Graph-Mutation
use std::rc::Rc;
use std::cell::RefCell;
struct Node {
name: String,
neighbors: RefCell<Vec<Rc<Node>>>,
}
impl Node {
fn new(name: impl Into<String>) -> Rc<Self> {
Rc::new(Node {
name: name.into(),
neighbors: RefCell::new(Vec::new()),
})
}
fn connect(&self, other: Rc<Node>) {
self.neighbors.borrow_mut().push(other);
}
}
fn main() {
let a = Node::new("A");
let b = Node::new("B");
let c = Node::new("C");
a.connect(Rc::clone(&b));
a.connect(Rc::clone(&c));
b.connect(Rc::clone(&c));
// c ist von a, b und main referenziert → strong_count = 3
assert_eq!(Rc::strong_count(&c), 3);
}Graph mit shared Nodes und Mutation. Eine Rc<Node>-Datenstruktur, in der jeder Node seine Nachbarn dynamisch erweitern kann.
Pub-Sub mit Threads
use std::sync::{Arc, Mutex};
use std::thread;
struct EventBus {
subscribers: Mutex<Vec<Box<dyn Fn(&str) + Send + 'static>>>,
}
impl EventBus {
fn new() -> Arc<Self> {
Arc::new(EventBus { subscribers: Mutex::new(Vec::new()) })
}
fn subscribe<F>(&self, callback: F)
where F: Fn(&str) + Send + 'static
{
self.subscribers.lock().unwrap().push(Box::new(callback));
}
fn send(&self, msg: &str) {
let subs = self.subscribers.lock().unwrap();
for s in subs.iter() { s(msg); }
}
}
fn main() {
let bus = EventBus::new();
bus.subscribe(|m| println!("Listener A: {m}"));
bus.subscribe(|m| println!("Listener B: {m}"));
let bus2 = Arc::clone(&bus);
thread::spawn(move || {
bus2.send("From thread");
}).join().unwrap();
bus.send("From main");
}EventBus mit Closures als Subscriber. Arc + Mutex erlauben sicheres Sharing zwischen Threads.
Counter-Service
use std::sync::{Arc, Mutex};
use std::thread;
use std::collections::HashMap;
struct CounterService {
counts: Mutex<HashMap<String, u64>>,
}
impl CounterService {
fn new() -> Arc<Self> {
Arc::new(CounterService { counts: Mutex::new(HashMap::new()) })
}
fn increment(&self, key: &str) {
*self.counts.lock().unwrap().entry(key.to_string()).or_insert(0) += 1;
}
fn get(&self, key: &str) -> u64 {
self.counts.lock().unwrap().get(key).copied().unwrap_or(0)
}
}
fn main() {
let svc = CounterService::new();
let mut handles = Vec::new();
for i in 0..5 {
let svc = Arc::clone(&svc);
handles.push(thread::spawn(move || {
for _ in 0..100 {
svc.increment(&format!("worker-{i}"));
}
}));
}
for h in handles { h.join().unwrap(); }
for i in 0..5 {
println!("worker-{i}: {}", svc.get(&format!("worker-{i}")));
}
}Geteilter Counter-Service mit thread-safer HashMap. Jeder Thread inkrementiert seinen eigenen Schlüssel.
Geteilter Cache (multi-threaded)
use std::sync::{Arc, RwLock};
use std::collections::HashMap;
use std::thread;
struct Cache {
data: RwLock<HashMap<String, String>>,
}
impl Cache {
fn new() -> Arc<Self> {
Arc::new(Cache { data: RwLock::new(HashMap::new()) })
}
fn put(&self, key: String, value: String) {
self.data.write().unwrap().insert(key, value);
}
fn get(&self, key: &str) -> Option<String> {
self.data.read().unwrap().get(key).cloned()
}
}
fn main() {
let cache = Cache::new();
cache.put(String::from("user-1"), String::from("Alice"));
cache.put(String::from("user-2"), String::from("Bob"));
let mut handles = Vec::new();
for i in 0..4 {
let cache = Arc::clone(&cache);
handles.push(thread::spawn(move || {
let key = format!("user-{}", (i % 2) + 1);
if let Some(v) = cache.get(&key) {
println!("Thread {i}: {key} -> {v}");
}
}));
}
for h in handles { h.join().unwrap(); }
}Arc<RwLock<HashMap>> für ein read-heavy Cache. Viele Threads können parallel lookup'en, nur Writes sind serialisiert.
Reaktiver State (single-threaded)
use std::rc::Rc;
use std::cell::RefCell;
struct State<T> {
value: RefCell<T>,
listeners: RefCell<Vec<Box<dyn Fn(&T)>>>,
}
impl<T: Clone + 'static> State<T> {
fn new(initial: T) -> Rc<Self> {
Rc::new(State {
value: RefCell::new(initial),
listeners: RefCell::new(Vec::new()),
})
}
fn observe(&self, callback: impl Fn(&T) + 'static) {
self.listeners.borrow_mut().push(Box::new(callback));
}
fn set(&self, new_value: T) {
*self.value.borrow_mut() = new_value;
let value = self.value.borrow();
for l in self.listeners.borrow().iter() {
l(&value);
}
}
}
fn main() {
let s = State::new(0);
s.observe(|v| println!("Listener: {v}"));
s.set(42); // Listener wird benachrichtigt
s.set(100);
}Reaktiver State mit Listener-Notification. Single-threaded, klassisches Rc<RefCell>-Pattern. Für UI-Frameworks wie Iced relevant.
Interessantes
Rc> = shared mutable, single-threaded.
Rc für mehrere Owner, RefCell für Mutation hinter geteilten Referenzen. Klassisch für Graphen, DLLs, Observer-Pattern, reaktiven State.
Arc> = shared mutable, threads.
Arc für Owner-Sharing zwischen Threads, Mutex für exklusive Mutation. Standardform für mutable Shared State in Threading-Code.
Arc> = read-heavy thread-shared.
Variante für viele Reader, wenige Writer. Skaliert besser als Mutex, wenn Read-Sektionen lang und Reads häufig sind.
Rc-Klone und Borrows sind getrennt.
Rc::clone() ändert den Owner-Counter, beeinflusst borrow()/borrow_mut() nicht. Die XOR-Regel gilt nur für die Borrow-Aufrufe — du kannst beliebig viele Rc-Klone haben.
Doppel-Borrow im selben Thread = Panic.
RefCell prüft zur Laufzeit. let g = r.borrow(); r.borrow_mut(); → Panic. Lösung: Borrow-Lifetimes minimieren, in kleinen Scopes halten.
Mehrere Locks bei Mutex = Deadlock-Risiko.
Bei Arc-Mutex-Pattern mit mehreren Locks immer konsistente Lock-Reihenfolge. Sonst potentielles Deadlock zwischen Threads.
Weak gegen Reference-Cycles.
Bei beidirektionalen Strukturen (Eltern↔Kind, Subject↔Observer): owned Richtung mit Rc/Arc, back-link mit Weak. Sonst Memory-Leak.
Code-Smell bei Über-Verwendung.
Wenn überall Rc<RefCell> steht, ist oft das Datenmodell falsch. Alternativen prüfen: Arena+Index, Channels, klares Ownership mit &-Borrowing.
Weiterführende Ressourcen
Externe Quellen
- The Rust Book – Allowing Multiple Owners with Rc and Interior Mutability
- The Rust Book – Shared-State Concurrency
- std::rc::Rc
- std::sync::Arc