Arc<T> (Atomic Reference Counted) ist die thread-sichere Version von Rc. Konzeptionell identisch — ein Counter zählt die Klone, der letzte räumt auf — aber der Counter wird mit atomic-Operationen modifiziert. Damit kann ein Arc<T> zwischen Threads verschickt werden, ohne dass Race Conditions am Counter entstehen. Praktisch ist Arc der Standard-Smart-Pointer für Multi-Threading-Code: ein Konfigurations-Wert, der von mehreren Worker-Threads gelesen wird, eine Lookup-Tabelle, die zwischen Async-Tasks geteilt wird, ein Subscription-Manager mit globalem State — überall, wo Daten zwischen Threads geteilt werden müssen und das nicht über Channels läuft, ist Arc der Default.
Was ist Arc?
Arc<T> ist mechanisch fast identisch zu Rc<T>: ein Heap-allozierter Wert plus Reference-Counter, geklont wird der Counter inkrementiert, beim Drop dekrementiert. Der einzige (aber zentrale) Unterschied: alle Counter-Operationen sind atomic.
use std::sync::Arc;
use std::thread;
fn main() {
let a = Arc::new(String::from("shared"));
let handles: Vec<_> = (0..3).map(|i| {
let a = Arc::clone(&a);
thread::spawn(move || {
println!("Thread {i}: {a}");
})
}).collect();
for h in handles {
h.join().unwrap();
}
assert_eq!(Arc::strong_count(&a), 1); // alle Threads beendet
}Der Unterschied zu Rc: das Verschicken in thread::spawn (move ||) funktioniert. Bei Rc würde der Compiler ablehnen mit Rc<i32> cannot be sent between threads safely. Arc<i32> darf in den Thread, weil es Send und Sync ist.
Send + Sync — die Thread-Garantien
Zwei Marker-Traits machen Arc thread-tauglich:
Send: ein Wert dieses Typs darf per Move-Operation zwischen Threads verschoben werden.Sync: eine Referenz auf diesen Typ darf zwischen Threads geteilt werden (formal:T: Sync⇔&T: Send).
Arc<T> ist Send + Sync, wenn T: Send + Sync. Das heißt: solange der innere Typ thread-sicher ist, ist die Arc-Hülle es auch.
Klassische thread-sichere Typen:
i32,f64,boolund alle PrimitiveString,Vec,HashMap(ohne Interior Mutability)- Eigene Structs aus thread-sicheren Feldern
Nicht-thread-sicher (kein Sync):
Cell,RefCell— Interior Mutability ohne LockRc— non-atomic Counter
Daher gibt es keine sinnvolle Arc<RefCell<T>> — das wäre für Threads ein Data-Race-Risiko. Für thread-safe Interior Mutability nimmst du Arc<Mutex<T>> oder Arc<RwLock<T>>.
Arc vs. Rc — der Performance-Vergleich
Atomic-Operationen sind teurer als normale Lese-/Schreib-Operationen. Konkret:
Rc::clone: ein normaler Counter-Increment, wenige Nanosekunden.Arc::clone: eine Atomic-Increment-Operation, typisch 5–50× langsamer alsRc::cloneauf modernen CPUs (je nach Cache-Status, Architektur, Contention).
In normalen APIs ist das irrelevant. Der Counter-Increment liegt im Nanosekunden-Bereich, deine Funktionen brauchen Mikrosekunden bis Millisekunden. In einem extrem heißen Loop mit Millionen clone-Aufrufen pro Sekunde könnte der Unterschied messbar werden — dann lohnt sich aber meist ein anderes Design (Pool, Arena, direkt Owned-Werte).
Pragma: wenn du Threads brauchst, nimm Arc. Wenn nicht, nimm Rc. Die Performance-Frage steht selten in der Mitte.
Arc allein gibt nur Lesezugriff
Wie Rc gibt auch Arc<T> nur lesenden Zugriff. Mutation des inneren Werts ist durch eine geteilte Referenz nicht möglich.
use std::sync::Arc;
fn main() {
let a = Arc::new(String::from("Hi"));
let b = Arc::clone(&a);
// a.push_str(" World");
// FEHLER: trait `DerefMut` is not implemented for `Arc<String>`
println!("{a}");
println!("{b}");
}Für gemeinsame Mutation über Threads ist das Standard-Pattern Arc<Mutex<T>> oder Arc<RwLock<T>> — der Arc trägt die Daten zwischen den Threads, der Mutex/RwLock kümmert sich um die Synchronisation. Beide bekommen eigene Artikel weiter unten.
Arc::clone in Threads — das Standardpattern
Das mit Abstand häufigste Arc-Pattern:
use std::sync::Arc;
use std::thread;
fn main() {
let data = Arc::new(vec![1, 2, 3, 4, 5]);
let mut handles = Vec::new();
for i in 0..3 {
// Pro Thread: ein eigener Klon des Arc
let data = Arc::clone(&data);
let h = thread::spawn(move || {
let sum: i32 = data.iter().sum();
println!("Thread {i}: sum = {sum}");
});
handles.push(h);
}
for h in handles {
h.join().unwrap();
}
}Wichtig: das let data = Arc::clone(&data); vor dem move || ist Pflicht. Ohne den Klon würde der move den einzigen Arc in die Closure schieben, der äußere wäre weg und der nächste Thread könnte ihn nicht mehr klonen.
Pro Thread brauchst du einen eigenen Klon, der dann in die Thread-Closure gemoved wird. Alle Klone zeigen auf denselben Heap-Wert, sind aber unabhängige Arc-Instanzen.
Arc und Tokio
In Tokio (async-Runtime) sieht das praktisch genauso aus — Tokio-Tasks sind quasi Threads, und ein Arc ist die Standard-Form für geteilten State.
// Erfordert: tokio = { version = "1", features = ["full"] }
use std::sync::Arc;
# async fn run() {
let config = Arc::new(String::from("api.example.com"));
let handles: Vec<_> = (0..5).map(|i| {
let config = Arc::clone(&config);
tokio::spawn(async move {
println!("Task {i}: {config}");
})
}).collect();
for h in handles {
h.await.unwrap();
}
# }Tokio-Tasks brauchen alle Send + 'static. Mit Arc<T> (T: Send + Sync + 'static) erfüllst du beide Bedingungen. Mehr im Async-Kapitel.
Weak<T> für Arc — Cycle-Vermeidung
Wie Rc hat auch Arc eine Weak<T>-Variante zur Vermeidung von Reference-Cycles. Sie verhält sich völlig analog:
use std::sync::{Arc, Weak};
fn main() {
let strong = Arc::new(String::from("data"));
let weak: Weak<String> = Arc::downgrade(&strong);
// strong existiert noch → upgrade liefert Some
if let Some(s) = weak.upgrade() {
println!("Alive: {s}");
}
drop(strong);
// strong ist gedroppt → upgrade liefert None
assert!(weak.upgrade().is_none());
}Arc::downgrade(&arc) erzeugt eine Weak<T>. weak.upgrade() versucht, sie wieder zu einem starken Arc zu machen — gelingt nur, wenn noch mindestens ein starker Klon existiert.
Klassische Anwendung: Eltern-Kind-Strukturen, in denen das Kind eine Referenz auf das Eltern-Element halten soll, aber der Eltern-Wert beim Verschwinden des Owners freigegeben werden können soll. Identisch zum Rc<Weak>-Pattern.
Arc::make_mut — Copy-on-Write auch hier
Arc::make_mut gibt mutable Zugriff: wenn nur ein starker Klon existiert (strong_count == 1 und weak_count == 0), wird in-place mutiert; sonst wird vorher klont.
use std::sync::Arc;
fn main() {
let mut a = Arc::new(String::from("hello"));
let b = Arc::clone(&a);
// a hat strong_count = 2 → make_mut macht eine Kopie:
Arc::make_mut(&mut a).push_str(" world");
assert_eq!(*a, "hello world");
assert_eq!(*b, "hello");
}Identisch zu Rc::make_mut, nur thread-sicher. Sehr elegant für „read-heavy"-Workloads, in denen Mutation eine seltene Ausnahme ist.
Wann Arc, wann nicht?
Eine gute Entscheidung in vier Schritten:
- Brauche ich überhaupt geteiltes Eigentum? Wenn der Wert nur einen Owner hat (auch wenn er gemoved wird), brauchst du kein Rc/Arc. Box oder Stack-Wert reicht.
- Sind Threads im Spiel? Wenn nein, ist
Rcdie schnellere Wahl. Wenn ja, brauchst duArc. - Brauche ich Mutation? Wenn ja, kommt zusätzlich
MutexoderRwLockins Spiel — siehe nächste Artikel. - Liest du nur in Hot-Loops? Dann ist auch Arc fast kostenlos. Counter-Inkrementierung nur beim Klonen, nicht bei jedem Read.
Eine häufige Falle: Arc<T> für single-threaded Code zu nehmen, „falls man später mal threadsicherheit braucht". Das ist Premature Optimization in die falsche Richtung — du zahlst die Atomic-Kosten ohne Nutzen. Pragma: nimm Rc, und wenn du später Threads brauchst, ist der Wechsel zu Arc ein simpler Text-Replace.
Praxis: Arc im echten Code
Worker-Threads mit geteilter Konfiguration
use std::sync::Arc;
use std::thread;
struct Config {
workers: u32,
api_url: String,
timeout_ms: u32,
}
fn main() {
let config = Arc::new(Config {
workers: 4,
api_url: String::from("https://api.example.com"),
timeout_ms: 5000,
});
let handles: Vec<_> = (0..config.workers).map(|i| {
let config = Arc::clone(&config);
thread::spawn(move || {
println!("Worker {i}: target={}, timeout={}ms",
config.api_url, config.timeout_ms);
// ... echte Arbeit ...
})
}).collect();
for h in handles {
h.join().unwrap();
}
}Klassisches Pattern. Eine Config wird einmal geladen, in einen Arc gelegt, an jeden Worker-Thread per Klon übergeben. Alle Threads sehen dieselben Werte, der Heap-Speicher wird nach dem letzten Thread freigegeben.
Geteilter, immutabler Cache
use std::sync::Arc;
use std::collections::HashMap;
use std::thread;
fn main() {
let mut cache = HashMap::new();
cache.insert(String::from("user-1"), String::from("Alice"));
cache.insert(String::from("user-2"), String::from("Bob"));
cache.insert(String::from("user-3"), String::from("Charlie"));
let cache = Arc::new(cache);
let handles: Vec<_> = ["user-1", "user-3", "user-2"]
.into_iter()
.map(|key| {
let cache = Arc::clone(&cache);
thread::spawn(move || {
if let Some(name) = cache.get(key) {
println!("{key} -> {name}");
}
})
})
.collect();
for h in handles {
h.join().unwrap();
}
}Ein Lookup, der einmal aufgebaut und dann parallel gelesen wird. Kein Mutex nötig, weil nur lesend zugegriffen wird — Arc<HashMap> reicht aus, und HashMap ist Sync, solange ihre Inhalte Sync sind.
Geteilte Logger-Instanz
use std::sync::Arc;
use std::thread;
struct Logger {
prefix: String,
}
impl Logger {
fn log(&self, msg: &str) {
println!("[{}] {msg}", self.prefix);
}
}
fn main() {
let logger = Arc::new(Logger { prefix: String::from("APP") });
let handles: Vec<_> = (0..4).map(|i| {
let logger = Arc::clone(&logger);
thread::spawn(move || {
logger.log(&format!("Thread {i} running"));
})
}).collect();
for h in handles {
h.join().unwrap();
}
}Ein read-only Logger, der von vielen Threads geteilt wird. Solange der Logger keinen mutable State hat (kein Counter, keine Liste), reicht Arc ohne Mutex.
Arc-Weak in Pub-Sub
use std::sync::{Arc, Weak, Mutex};
use std::thread;
trait Subscriber: Send + Sync {
fn notify(&self, msg: &str);
}
struct Broker {
subscribers: Mutex<Vec<Weak<dyn Subscriber>>>,
}
impl Broker {
fn new() -> Self {
Broker { subscribers: Mutex::new(Vec::new()) }
}
fn subscribe(&self, s: Weak<dyn Subscriber>) {
self.subscribers.lock().unwrap().push(s);
}
fn send(&self, msg: &str) {
let mut subs = self.subscribers.lock().unwrap();
subs.retain(|w| {
if let Some(s) = w.upgrade() {
s.notify(msg);
true
} else {
false
}
});
}
}
struct PrintSub;
impl Subscriber for PrintSub {
fn notify(&self, msg: &str) {
println!("Received: {msg}");
}
}
fn main() {
let broker = Arc::new(Broker::new());
let sub: Arc<dyn Subscriber> = Arc::new(PrintSub);
broker.subscribe(Arc::downgrade(&sub));
let broker2 = Arc::clone(&broker);
thread::spawn(move || {
broker2.send("Hello from thread");
}).join().unwrap();
}Broker hält schwache Referenzen — wenn ein Subscriber dropped, sieht das der Broker und entfernt ihn. Mit Arc<dyn Subscriber> und Weak<dyn Subscriber>: thread-safer Observer-Pattern.
Geteilte Trait-Objekt-Liste
use std::sync::Arc;
use std::thread;
trait Plugin: Send + Sync {
fn name(&self) -> String;
fn execute(&self, input: &str) -> String;
}
struct UppercasePlugin;
impl Plugin for UppercasePlugin {
fn name(&self) -> String { String::from("uppercase") }
fn execute(&self, input: &str) -> String { input.to_uppercase() }
}
fn main() {
let plugins: Arc<Vec<Arc<dyn Plugin>>> = Arc::new(vec![
Arc::new(UppercasePlugin),
]);
let handles: Vec<_> = (0..3).map(|i| {
let plugins = Arc::clone(&plugins);
thread::spawn(move || {
let input = format!("hello {i}");
for p in plugins.iter() {
println!("{}: {}", p.name(), p.execute(&input));
}
})
}).collect();
for h in handles {
h.join().unwrap();
}
}Plugin-Registry, die zwischen Threads geteilt wird. Arc<Vec<Arc<dyn Plugin>>> — der äußere Arc trägt die Liste, der innere Arc jedes einzelne Plugin. Beide tragen den Trait-Bound Send + Sync, damit sie thread-safe sind.
Geteilter Counter via Atomic
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::thread;
fn main() {
let counter = Arc::new(AtomicUsize::new(0));
let handles: Vec<_> = (0..10).map(|_| {
let counter = Arc::clone(&counter);
thread::spawn(move || {
for _ in 0..1000 {
counter.fetch_add(1, Ordering::Relaxed);
}
})
}).collect();
for h in handles {
h.join().unwrap();
}
assert_eq!(counter.load(Ordering::Relaxed), 10_000);
}Wenn die geteilten Daten ein atomic-Primitive sind (AtomicUsize, AtomicBool, etc.), brauchst du keinen Mutex. Arc + Atomic ist die schlankste Form für einfache Counter, Flags etc. — mehr im Concurrency-Kapitel.
Interessantes
Arc = Atomic Reference Counted, thread-safe.
Mechanisch identisch zu Rc, aber mit atomic-Counter-Operationen. Send + Sync, wenn T es ist. Standard für Multi-Threading-Code mit geteiltem Eigentum.
Arc::clone(&a) ist die idiomatische Form.
Wie bei Rc: macht klar, dass nur der Counter inkrementiert wird, kein Deep-Copy. Konvention bevorzugt die explizite Form gegenüber a.clone().
Pro Thread ein eigener Arc-Klon.
Vor move || einen Arc::clone(&shared) machen und den in die Closure schieben. Jeder Thread hat seinen Klon, alle zeigen auf denselben Heap-Wert.
Arc gibt nur lesenden Zugriff.
Mutation über Threads braucht zusätzlich Mutex oder RwLock. Standard-Pattern: Arc<Mutex<T>> für schreibend-geteilten State, Arc<RwLock<T>> für read-heavy.
Arc ist teurer als Rc — aber nur beim Clonen/Droppen.
Atomic-Operations am Counter sind 5–50× langsamer als non-atomic. In Hot-Loops mit massiv-vielen Clones messbar; im Normal-Code irrelevant.
Weak auch für Arc — gegen Reference-Cycles.
Arc::downgrade(&arc) erzeugt Weak. weak.upgrade() liefert Option<Arc<T>>. Identisches Pattern wie bei Rc, nur thread-safe.
Arc::make_mut für Copy-on-Write.
Wenn strong_count == 1: in-place mutieren. Sonst: vorher klonen. Eleganter Mechanismus für „selten mutierte" Daten.
Faustregel: Threads → Arc. Sonst Rc.
Single-threaded Code mit Rc anfangen, bei Bedarf wechseln. Keine Premature Optimization in beide Richtungen — Arc „für später" ist ungenötigter Overhead, Rc plus später Threads zu wollen ist eine kleine Text-Replace.
Weiterführende Ressourcen
Externe Quellen
- The Rust Book – Shared-State Concurrency
- std::sync::Arc
- std::sync::Weak
- Rust Atomics and Locks (Mara Bos)