Arc<T
Arc<T>
Level 3 — Ownership & Borrowing Thread-safe version of
Rc<T>using atomic reference counting.
1. Prerequisites
Rc<T>— You must understand how basic Reference Counting works first.- Ownership — The fundamental rule that both
RcandArcare designed to bypass.
2. Term Category
Rust-specific (the concurrent equivalent): While many languages use heavy, universal Garbage Collectors to manage memory across threads, Rust uses a specialized, explicit Atomic Reference Counter.
3. Explanation
(1) Design Motivation — "Why did we design this?"
We know that Rc<T> allows multiple owners to share data by keeping an integer count of how many owners exist. But if you try to pass an Rc into a background thread, the compiler throws a massive error. Why?
Because updating a standard integer across multiple threads is dangerous! If Thread A and Thread B both try to increment the Rc count at the exact same nanosecond, the CPU might drop one of the updates. The count becomes corrupted. If the count hits 0 early, the data drops while a thread is still using it. If the count never hits 0, you get a permanent memory leak.
To fix this, we need Arc<T> (Atomic Reference Counted). It does the exact same thing as Rc, but uses special hardware CPU instructions ("Atomics") to guarantee that the integer count is updated perfectly and safely across threads without ever corrupting.
(2) Reality Metaphor
Imagine two people (Threads) standing in different rooms, trying to update a shared chalkboard tally (Rc). Because they can't coordinate, they might both walk up to the board at the exact same second, see the number 1, erase it, and both write 2. They just added two viewers, but the board only says 2 instead of 3! The tally is corrupted.
Arc<T> is like replacing the chalkboard with a heavy, mechanical turnstile. No matter how fast people push through it simultaneously, the mechanical gears lock up and physically force the count to increment perfectly, one by one.
(3) Rust Code Examples
Short Snippet (Sharing across threads)
Using Arc looks identical to using Rc. You just swap the names.
use std::sync::Arc;
use std::thread;
fn main() {
// 1. Wrap data in an Atomic Reference Counter
let shared_data = Arc::new(String::from("Thread Secret"));
// 2. Clone it for the new thread (safely increments count to 2)
let data_for_thread = Arc::clone(&shared_data);
// 3. Move the clone into the background thread
let handle = thread::spawn(move || {
println!("Background thread reads: {}", data_for_thread);
}); // count drops to 1 here!
println!("Main thread reads: {}", shared_data);
handle.join().unwrap();
} // count drops to 0 here. String is dropped!
4. Common Mistakes & Pitfalls
Mistake 1: Misunderstanding Arc T Scoping and Lifecycle Rules
The mistake: Assuming Arc T instances remain valid beyond their declaring scope block or across asynchronous boundaries without explicit lifetime tracking.
Why it's wrong: Rust strictly enforces lexical scope boundaries and non-lexical lifetimes (NLL) at compile time. Accessing dropped values or failing to handle variable drop order results in compiler errors such as E0597 or E0382.
Incorrect:
fn get_ref() -> &str {
let s = String::from("arc_t_data");
&s // ❌ Error E0106/E0515: returns a reference to data owned by the current function
}
Fix:
fn get_string() -> String {
let s = String::from("arc_t_data");
s // Ownership of the String is transferred directly to the caller
}
Mistake 2: Mutating Arc T State Without Exclusive Ownership or mut Borrowing
The mistake: Attempting to mutate data associated with Arc T through an immutable reference &T or without specifying mut in variable declarations.
Why it's wrong: Rust's aliasing XOR mutability rule (&T for shared immutable access, &mut T for exclusive mutable access) prohibits mutating state through shared references unless interior mutability patterns (e.g. RefCell, Mutex) are explicitly used.
Incorrect:
fn update_val(data: &i32) {
// *data += 1; // ❌ Error E0594: cannot assign to `*data`, which is behind a `&` reference
}
Fix:
fn update_val(data: &mut i32) {
*data += 1; // Correct: exclusive mutable reference permits mutation
}
Mistake 3: Concurrent Access to Arc T Across Threads Without Send / Sync Guards
The mistake: Sharing non-thread-safe Arc T instances across OS threads via std::thread::spawn.
Why it's wrong: Types that do not implement Send or Sync marker traits cannot safely cross thread boundaries. The compiler prevents data races by raising compile errors E0277 (trait Send is not implemented).
Incorrect:
use std::rc::Rc;
use std::thread;
let rc = Rc::new(42);
// thread::spawn(move || { println!("{}", rc); }); // ❌ Error E0277: `Rc` cannot be sent between threads safely
Fix:
use std::sync::Arc;
use std::thread;
let arc = Arc::new(42);
thread::spawn(move || {
println!("{}", arc); // Correct: `Arc` implements `Send` and `Sync`
});
5. Practice Exercises
Exercise 1: Zero-Copy Concurrent Log Broadcast & Atomic Telemetry Pipeline
Scenario: In a high-throughput microservice, a central telemetry node loads log snapshot buffers into memory and broadcasts them to multiple background worker threads for parallel filtering without copying the buffer content. Each worker thread updates a shared atomic counter to record query metrics.
Task:
- Implement
SharedLogSnapshotholding anArc<Vec<String>>for the log lines and anArc<AtomicUsize>for query count. - Implement
process_logs_concurrentlyto spawn worker threads usingArc::clone, filter matching lines containing a keyword, and return results. - Write comprehensive unit tests verifying strong pointer counts, matching entries, atomic query counts, and pattern matching assertions.
Answer
Implementation
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::thread;
pub struct SharedLogSnapshot {
logs: Arc<Vec<String>>,
query_count: Arc<AtomicUsize>,
}
impl SharedLogSnapshot {
pub fn new(logs: Vec<String>) -> Self {
Self {
logs: Arc::new(logs),
query_count: Arc::new(AtomicUsize::new(0)),
}
}
pub fn filter_keyword(&self, keyword: &str) -> Vec<String> {
self.query_count.fetch_add(1, Ordering::SeqCst);
self.logs
.iter()
.filter(|line| line.contains(keyword))
.cloned()
.collect()
}
pub fn query_count(&self) -> usize {
self.query_count.load(Ordering::SeqCst)
}
pub fn strong_count(&self) -> usize {
Arc::strong_count(&self.logs)
}
}
pub fn process_logs_concurrently(
snapshot: &SharedLogSnapshot,
keyword: &str,
num_workers: usize,
) -> Vec<Vec<String>> {
let mut handles = Vec::with_capacity(num_workers);
for _ in 0..num_workers {
let logs_clone = Arc::clone(&snapshot.logs);
let count_clone = Arc::clone(&snapshot.query_count);
let kw = keyword.to_string();
let handle = thread::spawn(move || {
count_clone.fetch_add(1, Ordering::SeqCst);
logs_clone
.iter()
.filter(|line| line.contains(&kw))
.cloned()
.collect::<Vec<String>>()
});
handles.push(handle);
}
handles
.into_iter()
.map(|h| h.join().expect("Worker thread panicked"))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_concurrent_log_filtering() {
let logs = vec![
"INFO: System boot".to_string(),
"ERROR: Out of memory".to_string(),
"WARN: High CPU usage".to_string(),
"ERROR: Disk read failure".to_string(),
];
let snapshot = SharedLogSnapshot::new(logs);
// Verify initial strong pointer count is 1
assert_eq!(snapshot.strong_count(), 1);
let worker_results = process_logs_concurrently(&snapshot, "ERROR", 4);
// Verify all worker threads completed successfully and returned matching errors
assert_eq!(worker_results.len(), 4);
for res in &worker_results {
assert_eq!(res.len(), 2);
assert!(res[0].contains("ERROR"));
assert_ne!(res[0], res[1]);
}
// Verify total atomic query executions across threads
assert_eq!(snapshot.query_count(), 4);
// Verify strong reference count returned to 1 after worker threads exited
assert_eq!(snapshot.strong_count(), 1);
// Verify slice matching with matches!
let single_filter = snapshot.filter_keyword("WARN");
assert!(matches!(single_filter.as_slice(), [ref line] if line.contains("WARN")));
}
}
Technical Explanation
- Step-by-Step Technical Breakdown:
SharedLogSnapshot::newwraps aVec<String>in anArc, allocating a control block on the heap alongside the log buffer.process_logs_concurrentlyloopsnum_workerstimes, invokingArc::clone(&snapshot.logs)andArc::clone(&snapshot.query_count)for each worker.Arc::cloneincrements the atomic strong reference count without copying the string buffer vectors.thread::spawn(move || ...)transfers ownership of the clonedArchandles into each thread's closure frame.- Each thread executes its search, atomically updates
query_countviafetch_add, and exits. Joining the handles cleans up thread execution states and drops the thread-localArcreferences.
- Language Invariants & Thread Safety (
Send/Sync):Arc<T>implementsSendandSyncif and only ifT: Send + Sync. SinceVec<String>andAtomicUsizeboth implementSend + Sync,Arc<Vec<String>>can safely cross OS thread boundaries.Rc<T>cannot be used here because its reference count increment is non-atomic and lacksSend/Synctrait implementations.
- Lifetime & Ownership Implications:
- The closure passed to
thread::spawnrequires a'staticlifetime boundary because OS threads can theoretically outlive the caller's stack frame.Arcsatisfies'staticownership by taking owned heap data across thread boundaries. - When worker threads finish, their internal
Arcinstances are dropped, callingfetch_subatomically. Once all worker handles join and exit,strong_countdrops back to 1.
- The closure passed to
- Memory Layout:
Arc<Vec<String>>consists of a stack-allocated 8-byte pointer targeting an inline heap allocation formatted as[ strong: AtomicUsize | weak: AtomicUsize | data: Vec<String> ].- The underlying string buffer heap allocation is shared seamlessly across all workers with zero memory duplication.
- Edge Cases:
- If a worker thread panics during filtering,
handle.join()returns anErr(Box<dyn Any>). Theexpectunwraps this and propagates the failure cleanly while dropping the thread'sArchandle to avoid memory leaks.
- If a worker thread panics during filtering,
Exercise 2: Lock-Free Observer Registry with Arc<T> and Weak<T> Weak Reference Upgrades
Scenario: An event notification engine maintains a list of observer callbacks across worker services. To prevent cyclic reference memory leaks and avoid keeping observers alive beyond their domain scope, the event hub stores subscribers as Weak<Observer>. During event dispatches, the hub attempts to upgrade Weak<Observer> references to Arc<Observer>, safely skipping observers that have been dropped.
Task:
- Implement
Observercontainingid,name, and an atomic processed event count. - Implement
EventHubmaintainingsubscribers: Vec<Weak<Observer>>. - Implement
subscribe,publish, andprunemethods usingArc::downgradeandWeak::upgrade. - Write unit tests with assertions (
assert_eq!,assert!,assert_ne!,matches!) demonstrating dynamic subscriber drop, safe upgrade handling, and list pruning.
Answer
Implementation
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Weak};
pub struct Observer {
pub id: u64,
pub name: String,
processed_count: AtomicUsize,
}
impl Observer {
pub fn new(id: u64, name: &str) -> Self {
Self {
id,
name: name.to_string(),
processed_count: AtomicUsize::new(0),
}
}
pub fn notify(&self, _event: &str) {
self.processed_count.fetch_add(1, Ordering::SeqCst);
}
pub fn processed_count(&self) -> usize {
self.processed_count.load(Ordering::SeqCst)
}
}
pub struct EventHub {
subscribers: Vec<Weak<Observer>>,
}
impl EventHub {
pub fn new() -> Self {
Self {
subscribers: Vec::new(),
}
}
pub fn subscribe(&mut self, observer: &Arc<Observer>) {
self.subscribers.push(Arc::downgrade(observer));
}
pub fn publish(&self, event: &str) -> usize {
let mut delivered = 0;
for weak_sub in &self.subscribers {
if let Some(observer) = weak_sub.upgrade() {
observer.notify(event);
delivered += 1;
}
}
delivered
}
pub fn prune(&mut self) {
self.subscribers.retain(|weak_sub| weak_sub.strong_count() > 0);
}
pub fn subscriber_count(&self) -> usize {
self.subscribers.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_weak_observer_lifecycle_and_pruning() {
let mut hub = EventHub::new();
let obs1 = Arc::new(Observer::new(1, "MetricsWorker"));
let obs2 = Arc::new(Observer::new(2, "AuditLogger"));
hub.subscribe(&obs1);
hub.subscribe(&obs2);
assert_eq!(hub.subscriber_count(), 2);
assert_eq!(Arc::strong_count(&obs1), 1);
assert_eq!(Arc::weak_count(&obs1), 1);
// Publish event while both observers exist
let delivered = hub.publish("USER_LOGIN");
assert_eq!(delivered, 2);
assert_eq!(obs1.processed_count(), 1);
assert_eq!(obs2.processed_count(), 1);
// Explicitly drop obs2
drop(obs2);
// Publish event when one observer is dropped
let delivered_after_drop = hub.publish("USER_LOGOUT");
assert_eq!(delivered_after_drop, 1);
assert_eq!(obs1.processed_count(), 2);
assert!(obs1.processed_count() > 0);
// Verify unpruned count vs pruned count
assert_ne!(hub.subscriber_count(), 1); // Weak handle still exists in Vec
hub.prune();
assert_eq!(hub.subscriber_count(), 1); // Successfully pruned
// Match subscriber upgrade using matches!
let remaining_weak = &hub.subscribers[0];
let upgraded = remaining_weak.upgrade();
assert!(matches!(upgraded, Some(ref obs) if obs.id == 1));
}
}
Technical Explanation
- Step-by-Step Technical Breakdown:
Arc::downgrade(&obs)creates a non-owningWeak<Observer>handle and increments the atomicweakpointer counter without alteringstrong_count.- During
publish,weak_sub.upgrade()attempts to convertWeak<Observer>intoArc<Observer>. Ifstrong_count > 0, it atomically incrementsstrong_countand returnsSome(Arc<Observer>). If the originalArcwas dropped, it returnsNone. prunefilters out handles whereweak_sub.strong_count() == 0, reclaiming administrative vector slots.
- Language Invariants & Cycle Prevention:
- Holding strong
Arcreferences inside registry collections causes reference cycles or memory leaks if subscribers are never explicitly unregistered. - Using
Weak<T>ensures that observer instances drop as soon as their domain owners out of scope, eliminating memory leaks while keeping dispatches safe.
- Holding strong
- Lifetime & Ownership Implications:
Weak<T>does not grant direct access toT. Access requires temporary upgrading toArc<T>, which holds a strong reference for the scope ofSome(observer).- Once
notifyfinishes,observerfalls out of scope, dropping the upgraded strong reference back to zero if no other strong references exist.
- Memory Layout:
- An
Arcallocation header keeps[ strong: AtomicUsize | weak: AtomicUsize | data: T ]. - When
strongreaches 0,data(Observer) is dropped immediately and its internal buffer is deallocated. However, the control block[ strong | weak ]remains allocated untilweakalso drops to 0.
- An
- Edge Cases:
- Concurrent drop during
upgrade(): Atomic CPU instructions ensure thatupgrade()either successfully incrementsstrong_countbefore drop completes or observes0and returnsNone, guaranteeing zero data races.
- Concurrent drop during
Exercise 3: Multithreaded Metric Aggregation Engine with Arc<Mutex<T>> Interior Mutability
Scenario: In a parallel stream processing platform, multiple worker threads process chunks of financial transactions concurrently and aggregate runtime metrics (total processing amount, success counts, error codes) into a shared accumulator. Since Arc<T> only permits immutable &T borrows across threads, it must be paired with a Mutex<T> (Arc<Mutex<MetricsAccumulator>>) to achieve thread-safe interior mutability.
Task:
- Implement
Transactionstruct andMetricsAccumulatorstruct. - Implement
process_transaction_batchesspawning worker threads for each batch. - Use
Arc::cloneandMutex::lockto update shared state safely across threads. - Write unit tests with explicit assertions (
assert_eq!,assert!,assert_ne!,matches!) verifying multithreaded metrics aggregation, strong reference counter transitions, and mutex lock safety.
Answer
Implementation
use std::sync::{Arc, Mutex};
use std::thread;
#[derive(Debug, Clone)]
pub struct Transaction {
pub id: u64,
pub amount: f64,
pub is_valid: bool,
pub error_code: Option<String>,
}
#[derive(Debug, Default)]
pub struct MetricsAccumulator {
pub total_amount: f64,
pub success_count: u64,
pub failure_count: u64,
pub errors: Vec<String>,
}
impl MetricsAccumulator {
pub fn record(&mut self, tx: &Transaction) {
if tx.is_valid {
self.total_amount += tx.amount;
self.success_count += 1;
} else {
self.failure_count += 1;
if let Some(err) = &tx.error_code {
self.errors.push(err.clone());
}
}
}
}
pub fn process_transaction_batches(
batches: Vec<Vec<Transaction>>,
shared_metrics: Arc<Mutex<MetricsAccumulator>>,
) {
let mut handles = Vec::with_capacity(batches.len());
for batch in batches {
let metrics_clone = Arc::clone(&shared_metrics);
let handle = thread::spawn(move || {
for tx in &batch {
let mut guard = metrics_clone
.lock()
.expect("Mutex poisoned by thread panic");
guard.record(tx);
}
});
handles.push(handle);
}
for handle in handles {
handle.join().expect("Worker thread panicked");
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_concurrent_metrics_aggregation() {
let metrics = Arc::new(Mutex::new(MetricsAccumulator::default()));
// Initial strong count is 1
assert_eq!(Arc::strong_count(&metrics), 1);
let batch1 = vec![
Transaction { id: 1, amount: 150.0, is_valid: true, error_code: None },
Transaction { id: 2, amount: 0.0, is_valid: false, error_code: Some("ERR_INSUFFICIENT_FUNDS".to_string()) },
];
let batch2 = vec![
Transaction { id: 3, amount: 250.5, is_valid: true, error_code: None },
Transaction { id: 4, amount: 99.5, is_valid: true, error_code: None },
];
process_transaction_batches(vec![batch1, batch2], Arc::clone(&metrics));
// Strong pointer count resets back to 1 after threads join
assert_eq!(Arc::strong_count(&metrics), 1);
let final_metrics = metrics.lock().unwrap();
assert_eq!(final_metrics.success_count, 3);
assert_eq!(final_metrics.failure_count, 1);
assert_eq!(final_metrics.total_amount, 500.0);
assert_ne!(final_metrics.success_count, final_metrics.failure_count);
assert!(final_metrics.total_amount > 0.0);
// Verify recorded error code using matches!
assert!(matches!(final_metrics.errors.as_slice(), [ref err] if err == "ERR_INSUFFICIENT_FUNDS"));
}
}
Technical Explanation
- Step-by-Step Technical Breakdown:
Arc::new(Mutex::new(...))wraps the accumulator in a thread-safe interior mutability container.process_transaction_batchesclonesArcfor each worker thread, incrementingstrong_count.- Inside each worker thread loop,
metrics_clone.lock()requests exclusive access. Once acquired, it returns aMutexGuard, allowing mutable operationguard.record(tx). - Upon exiting scope at the end of each loop iteration or thread execution, RAII drops
MutexGuard, releasing the hardware lock for other threads.
- Language Invariants & Interior Mutability:
Arc<T>strictly provides shared immutable references (&T). It does not allow calling&mut Tmethods on the underlying data.- To modify data shared via
Arc, an interior mutability primitive implementingSync(such asMutex<T>orRwLock<T>) is required.RefCell<T>cannot be used across threads because it is!Sync.
- Lifetime & Ownership Implications:
- The
MutexGuard<'a, T>returned by.lock()borrows theMutexfor lifetime'a. The lock remains held until the guard is dropped. - Scoping lock guards tightly ensures lock contention between concurrent worker threads is minimized.
- The
- Memory Layout:
Arc<Mutex<MetricsAccumulator>>layout: Stack pointer ->[ strong: AtomicUsize | weak: AtomicUsize | Mutex { lock_state: AtomicU32, data: MetricsAccumulator } ].
- Edge Cases & Poisoning:
- If a worker thread panics while holding the
MutexGuard, theMutexenters a poisoned state. Subsequent.lock()calls returnErr(PoisonError). Calling.expect(...)or.unwrap()explicitly surfaces the panic and prevents corrupt state processing.
- If a worker thread panics while holding the
6. Related Terms
Rc<T>— The faster, single-threaded sibling.Mutex<T>— Often wrapped inside anArc(asArc<Mutex<T>>) to allow thread-safe mutation.RefCell<T>— The single-threaded way to mutate shared data (used withRc).AtomicTypes — Related concept:AtomicTypes.Condvar&Barrier— Related concept:Condvar&Barrier.RwLock<T>— Related concept:RwLock<T>.- Scoped Threads (
std::thread::scope) — Related concept: Scoped Threads (std::thread::scope). SendTrait — Related concept:SendTrait.- Smart Pointers (
Box,Rc,Arc) — Related concept: Smart Pointers (Box,Rc,Arc).
7. Key Takeaways
Arc<T>stands for Atomic Reference Counted.- It does the exact same thing as
Rc<T>(Shared Ownership), but it is Thread Safe. - It uses special CPU instructions to ensure the owner count is never corrupted, even if multiple threads clone it simultaneously.
- Because these atomic instructions have a slight performance cost, you should only use
Arcwhen dealing with threads. Otherwise, stick toRc. - Like
Rc, the data inside anArcis strictly read-only!