Rust 60 🦀 Thread-Safe Reference Counting (Arc)
The Rc<T> smart pointer provides shared ownership on a single thread, but the reference count is a plain integer. Incrementing it from two threads is a data race, and the compiler rejects sending an Rc across a thread boundary. The Arc<T> type — “atomic reference counted” — is the thread-safe counterpart. It uses an AtomicUsize for the count, which makes the increment and decrement atomic, so the count is correct even when the value is shared across threads. The cost is a small performance penalty for the atomic operations, which is why Arc is not the default. The rule is simple: use Rc when the sharing is confined to one thread, and Arc when the sharing crosses thread boundaries.
Key point: Arc<T> is Send and Sync when T is Send and Sync. Sharing an Arc across threads is safe only when the inner value is safe to access from multiple threads. An Arc<RefCell<T>> is not Sync, because RefCell<T> is not Sync, and the compiler rejects it. The correct pairing is Arc<Mutex<T>> or Arc<RwLock<T>>.
Why Arc exists
The data race problem. The Rc<T> count is a Cell<usize>. Two threads incrementing the count at the same time can read the same value, write the same result, and lose one of the increments. The count is then wrong, and the value is either freed too early or never freed. The Arc<T> count is an AtomicUsize, and the increment uses a compare-and-swap loop, so concurrent increments are correct.
The Send and Sync problem. Rc<T> is neither Send nor Sync, which means the compiler rejects sending it to another thread or sharing a reference to it. Arc<T> is Send and Sync when T is Send and Sync, which is what allows the value to be shared across threads.
The thread-spawn problem. The std::thread::spawn function requires its closure to be Send and 'static. A closure that captures an Rc<T> is rejected, because the Rc is not Send. A closure that captures an Arc<T> is accepted, because the Arc is Send.
The mutable-sharing problem. An Arc<T> provides immutable access to the inner value. Mutating through an Arc requires an interior-mutability type. The RefCell<T> is not Sync, so it cannot be used with Arc across threads. The Mutex<T> and RwLock<T> are the thread-safe interior-mutability types.
The performance problem. The atomic operations are slower than the non-atomic ones. An Arc::clone is a compare-and-swap loop, while an Rc::clone is a plain increment. On a single thread, the Rc is faster, and the difference is measurable in a tight loop. The Arc is the right choice only when the sharing crosses threads.
The poisoning problem. A Mutex<T> is poisoned when a thread panics while holding the lock. The lock method returns a Result that is Err if the mutex is poisoned. The Arc<Mutex<T>> pattern must handle the poisoning, either by propagating the error or by ignoring it with unwrap.
The deadlock problem. An Arc<Mutex<T>> can deadlock if two threads lock two mutexes in different orders. The standard advice is to lock mutexes in a consistent order, or to use a single mutex that protects multiple values, or to use a lock-free data structure.
a. The Arc API
Arc<T> has the same API as Rc<T>, with the atomic count in place of the plain count. The functions are named identically, which makes the migration straightforward.
The creation
use std::sync::Arc;
fn main() {
let value = Arc::new(5);
println!("{}", value); // 5
println!("{}", Arc::strong_count(&value)); // 1
}
The Arc::new function allocates the value on the heap and initializes the atomic strong count to 1.
The clone
fn main() {
let a = Arc::new(5);
let b = Arc::clone(&a);
println!("{}", Arc::strong_count(&a)); // 2
}
The Arc::clone function increments the atomic count. The clone is safe across threads.
The strong count
fn main() {
let a = Arc::new(5);
let b = Arc::clone(&a);
let c = Arc::clone(&a);
println!("{}", Arc::strong_count(&a)); // 3
}
The Arc::strong_count function returns the current strong count. The count is read atomically, so the value is a snapshot.
The try_unwrap
fn main() {
let a = Arc::new(5);
match Arc::try_unwrap(a) {
Ok(value) => println!("{}", value), // 5
Err(_) => println!("Still shared"),
}
}
The Arc::try_unwrap function returns the inner value if the strong count is 1. The function is the thread-safe counterpart of Rc::try_unwrap.
The ptr_eq
fn main() {
let a = Arc::new(5);
let b = Arc::clone(&a);
let c = Arc::new(5);
println!("{}", Arc::ptr_eq(&a, &b)); // true
println!("{}", Arc::ptr_eq(&a, &c)); // false
}
The Arc::ptr_eq function returns true if two Arc values point to the same allocation.
The into_raw and from_raw
use std::sync::Arc;
fn main() {
let a = Arc::new(5);
let raw = Arc::into_raw(a);
let b = unsafe { Arc::from_raw(raw) };
println!("{}", b); // 5
}
The Arc::into_raw and Arc::from_raw functions are the unsafe pair for passing an Arc through a C API.
The Send and Sync bounds
The Arc<T> type is Send and Sync when T is Send and Sync. The bounds are checked at compile time.
use std::sync::Arc;
use std::cell::RefCell;
fn main() {
let a = Arc::new(RefCell::new(5));
// Arc<RefCell<i32>> is not Sync.
// Sharing it across threads is rejected.
}
The RefCell<T> is not Sync, so the Arc<RefCell<T>> is not Sync, and the compiler rejects the sharing.
The Arc methods
| Method | Purpose |
|---|---|
Arc::new(value) | Create a new Arc |
Arc::clone(&arc) | Increment the count |
Arc::strong_count(&arc) | The strong count |
Arc::weak_count(&arc) | The weak count |
Arc::try_unwrap(arc) | Extract if unique |
Arc::ptr_eq(&a, &b) | Same allocation |
Arc::into_raw(arc) | Consume to a raw pointer |
Arc::from_raw(p) | Reconstruct from a raw pointer |
Arc::downgrade(&arc) | Create a Weak |
b. The Arc patterns
The Arc<T> type is used in a small set of recurring patterns. The patterns are the reason the type exists.
The shared read-only data
use std::sync::Arc;
use std::thread;
fn main() {
let data = Arc::new(vec![1, 2, 3, 4, 5]);
let mut handles = vec![];
for _ in 0..3 {
let d = Arc::clone(&data);
handles.push(thread::spawn(move || {
let sum: i32 = d.iter().sum();
println!("Sum: {}", sum);
}));
}
for h in handles {
h.join().unwrap();
}
}
The Arc provides shared read-only access to the vector. Each thread holds a clone, and the vector is not duplicated.
The Arc<Mutex> pattern
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let c = Arc::clone(&counter);
handles.push(thread::spawn(move || {
let mut num = c.lock().unwrap();
*num += 1;
}));
}
for h in handles {
h.join().unwrap();
}
println!("{}", *counter.lock().unwrap()); // 10
}
The Arc provides the shared ownership, and the Mutex provides the thread-safe mutation. The combination is the standard pattern for shared mutable state across threads.
The Arc<RwLock> pattern
use std::sync::{Arc, RwLock};
use std::thread;
fn main() {
let data = Arc::new(RwLock::new(vec![1, 2, 3]));
let mut handles = vec![];
// Readers
for _ in 0..5 {
let d = Arc::clone(&data);
handles.push(thread::spawn(move || {
let r = d.read().unwrap();
println!("Read: {:?}", *r);
}));
}
// Writer
{
let d = Arc::clone(&data);
handles.push(thread::spawn(move || {
let mut w = d.write().unwrap();
w.push(4);
}));
}
for h in handles {
h.join().unwrap();
}
}
The RwLock allows multiple readers or one writer. It is the right choice when reads are more common than writes.
The Arc pattern
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::thread;
fn main() {
let counter = Arc::new(AtomicUsize::new(0));
let mut handles = vec![];
for _ in 0..10 {
let c = Arc::clone(&counter);
handles.push(thread::spawn(move || {
c.fetch_add(1, Ordering::SeqCst);
}));
}
for h in handles {
h.join().unwrap();
}
println!("{}", counter.load(Ordering::SeqCst)); // 10
}
The AtomicUsize provides lock-free mutation. The pattern is faster than the Mutex for simple counters, because there is no lock.
The Arc pattern
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
fn main() {
let flag = Arc::new(AtomicBool::new(false));
let f = Arc::clone(&flag);
f.store(true, Ordering::SeqCst);
println!("{}", flag.load(Ordering::SeqCst)); // true
}
The AtomicBool is the atomic counterpart of the Cell<bool>. It is used for flags, one-shot initialization, and cancellation.
The Arc<AtomicPtr> pattern
use std::sync::Arc;
use std::sync::atomic::{AtomicPtr, Ordering};
fn main() {
let ptr = Arc::new(AtomicPtr::<i32>::new(std::ptr::null_mut()));
let value = Box::into_raw(Box::new(42));
ptr.store(value, Ordering::SeqCst);
let loaded = unsafe { *ptr.load(Ordering::SeqCst) };
println!("{}", loaded); // 42
}
The AtomicPtr<T> is the atomic counterpart of the raw pointer. It is used in lock-free data structures.
The pattern table
| Pattern | Use Case |
|---|---|
Arc<T> | Shared read-only data |
Arc<Mutex<T>> | Shared mutable state |
Arc<RwLock<T>> | Shared read-mostly state |
Arc<AtomicUsize> | Lock-free counter |
Arc<AtomicBool> | Atomic flag |
Arc<AtomicPtr<T>> | Lock-free pointer |
c. Arc versus Rc and the trade-offs
The choice between Arc and Rc is a matter of threading. The two types have the same API, and the migration is a rename plus the import change.
The Rc versus Arc
| Aspect | Rc<T> | Arc<T> |
|---|---|---|
| Count | Cell<usize> | AtomicUsize |
| Thread-safe | No | Yes |
Send | No | If T: Send + Sync |
Sync | No | If T: Send + Sync |
| Speed | Faster | Slower |
| Use case | Single thread | Multiple threads |
| Weak | std::rc::Weak | std::sync::Weak |
The performance
The atomic operations are slower than the plain operations. The difference is in the nanosecond range per operation, but it accumulates in a tight loop. The benchmark is: a million Rc::clone calls take about 10 milliseconds, and a million Arc::clone calls take about 40 milliseconds on a typical machine. The exact numbers vary by hardware.
The Send and Sync rules
The Arc<T> is Send if T: Send + Sync. The Send bound is required because the Arc can be moved to another thread. The Sync bound is required because the Arc can be shared across threads, and the inner value is accessed concurrently.
Arc<T>: Send + Sync ⟺ T: Send + Sync
The rule is the reason Arc<RefCell<T>> is rejected: RefCell<T> is not Sync, so the Arc is not Send or Sync.
The Mutex versus RwLock
| Aspect | Mutex<T> | RwLock<T> |
|---|---|---|
| Readers | One at a time | Many at a time |
| Writers | One at a time | One at a time |
| Overhead | Lower | Higher |
| Use case | Balanced | Read-mostly |
| Poisoning | Yes | Yes |
The Mutex is simpler and faster for balanced workloads. The RwLock is faster when reads dominate.
The poisoning
A Mutex is poisoned when a thread panics while holding the lock. The lock method returns Err if the mutex is poisoned. The unwrap call panics, and the expect call produces a custom message.
let value = mutex.lock().unwrap();
The unwrap is the common choice, because a poisoned mutex is a bug that should be surfaced.
The deadlock
A deadlock happens when two threads each hold a lock the other needs. The standard advice is to lock in a consistent order, or to use a single lock for related values, or to use a lock-free structure.
// ❌ Deadlock risk.
let a = mutex_a.lock();
let b = mutex_b.lock();
// ✅ Consistent order.
let a = mutex_a.lock();
let b = mutex_b.lock();
The two snippets look the same, but the first is a different thread that locks in the opposite order. The fix is to lock in the same order everywhere.
The atomic types
The AtomicUsize, AtomicBool, AtomicPtr, and the rest are the building blocks of lock-free code. They provide the operations that the Mutex uses internally, and they are available for the developer who needs them directly.
| Atomic | Operations |
|---|---|
AtomicUsize | load, store, fetch_add, compare_exchange |
AtomicBool | load, store, swap, compare_exchange |
AtomicPtr<T> | load, store, swap, compare_exchange |
AtomicI32 | load, store, fetch_add, fetch_sub |
The Ordering parameter controls the memory ordering. The SeqCst ordering is the safest and the slowest, and it is the default choice for the first implementation.
The Ordering types
| Ordering | Meaning |
|---|---|
Relaxed | No ordering guarantees |
Acquire | Loads are ordered after |
Release | Stores are ordered before |
AcqRel | Both Acquire and Release |
SeqCst | Total order across all threads |
The Relaxed ordering is the fastest and the least safe. The SeqCst ordering is the slowest and the safest. The Acquire and Release orderings are the middle ground, and they are the right choice for lock-free data structures.
Complete Example Session
// ============================================
// PART 1: THE BASIC ARC
// ============================================
use std::sync::Arc;
fn main() {
let a = Arc::new(5);
let b = Arc::clone(&a);
println!("{}", Arc::strong_count(&a)); // 2
println!("{}", *a); // 5
}
// ============================================
// PART 2: THE SHARED READ-ONLY DATA
// ============================================
use std::thread;
fn main() {
let data = Arc::new(vec![1, 2, 3, 4, 5]);
let mut handles = vec![];
for i in 0..3 {
let d = Arc::clone(&data);
handles.push(thread::spawn(move || {
println!("Thread {}: sum = {}", i, d.iter().sum::<i32>());
}));
}
for h in handles {
h.join().unwrap();
}
}
// ============================================
// PART 3: THE ARC<MUTEX<T>> PATTERN
// ============================================
use std::sync::Mutex;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let c = Arc::clone(&counter);
handles.push(thread::spawn(move || {
let mut num = c.lock().unwrap();
*num += 1;
}));
}
for h in handles {
h.join().unwrap();
}
println!("{}", *counter.lock().unwrap()); // 10
}
// ============================================
// PART 4: THE ARC<RWLOCK<T>> PATTERN
// ============================================
use std::sync::RwLock;
fn main() {
let data = Arc::new(RwLock::new(vec![1, 2, 3]));
let mut handles = vec![];
for _ in 0..5 {
let d = Arc::clone(&data);
handles.push(thread::spawn(move || {
let r = d.read().unwrap();
println!("Read: {:?}", *r);
}));
}
{
let d = Arc::clone(&data);
handles.push(thread::spawn(move || {
let mut w = d.write().unwrap();
w.push(4);
}));
}
for h in handles {
h.join().unwrap();
}
}
// ============================================
// PART 5: THE ATOMIC COUNTER
// ============================================
use std::sync::atomic::{AtomicUsize, Ordering};
fn main() {
let counter = Arc::new(AtomicUsize::new(0));
let mut handles = vec![];
for _ in 0..10 {
let c = Arc::clone(&counter);
handles.push(thread::spawn(move || {
c.fetch_add(1, Ordering::SeqCst);
}));
}
for h in handles {
h.join().unwrap();
}
println!("{}", counter.load(Ordering::SeqCst)); // 10
}
// ============================================
// PART 6: THE ATOMIC BOOL
// ============================================
use std::sync::atomic::AtomicBool;
fn main() {
let flag = Arc::new(AtomicBool::new(false));
let f = Arc::clone(&flag);
f.store(true, Ordering::SeqCst);
println!("{}", flag.load(Ordering::SeqCst)); // true
}
// ============================================
// PART 7: THE ARC<WEAK<T>> PATTERN
// ============================================
use std::sync::Weak;
fn main() {
let strong = Arc::new(5);
let weak: Weak<i32> = Arc::downgrade(&strong);
assert_eq!(Arc::strong_count(&strong), 1);
assert_eq!(Arc::weak_count(&strong), 1);
if let Some(rc) = weak.upgrade() {
println!("{}", rc); // 5
}
}
// ============================================
// PART 8: THE POISONING
// ============================================
use std::panic;
fn main() {
let mutex = Arc::new(Mutex::new(0));
let m = Arc::clone(&mutex);
let _ = thread::spawn(move || {
let _guard = m.lock().unwrap();
panic!("Oops");
}).join();
match mutex.lock() {
Ok(_) => println!("Lock acquired"),
Err(_) => println!("Mutex is poisoned"),
}
}
// ============================================
// PART 9: THE DEADLOCK
// ============================================
fn main() {
let a = Arc::new(Mutex::new(0));
let b = Arc::new(Mutex::new(0));
// ❌ This can deadlock:
// let _ga = a.lock().unwrap();
// let _gb = b.lock().unwrap();
// ✅ Consistent order:
let _ga = a.lock().unwrap();
let _gb = b.lock().unwrap();
}
// ============================================
// PART 10: THE COMPARISON
// ============================================
use std::rc::Rc;
use std::cell::RefCell;
fn single_thread() {
let data = Rc::new(RefCell::new(0));
*data.borrow_mut() += 1;
}
fn multi_thread() {
let data = Arc::new(Mutex::new(0));
*data.lock().unwrap() += 1;
}
The ten parts showed the basic Arc, the shared read-only data, the Arc<Mutex<T>> pattern, the Arc<RwLock<T>> pattern, the atomic counter, the atomic bool, the Arc<Weak<T>> pattern, the poisoning, the deadlock, and the comparison.
Quick Reference
The Arc API
| Method | Purpose |
|---|---|
Arc::new(value) | Create a new Arc |
Arc::clone(&arc) | Increment the count |
Arc::strong_count(&arc) | The strong count |
Arc::weak_count(&arc) | The weak count |
Arc::try_unwrap(arc) | Extract if unique |
Arc::ptr_eq(&a, &b) | Same allocation |
Arc::into_raw(arc) | Consume to a raw pointer |
Arc::from_raw(p) | Reconstruct |
Arc::downgrade(&arc) | Create a Weak |
The Rc vs Arc
| Aspect | Rc<T> | Arc<T> |
|---|---|---|
| Count | Cell<usize> | AtomicUsize |
| Thread-safe | No | Yes |
Send | No | If T: Send + Sync |
| Speed | Faster | Slower |
| Weak | std::rc::Weak | std::sync::Weak |
The Shared-Mutation Patterns
| Pattern | Use Case |
|---|---|
Arc<Mutex<T>> | Shared mutable state |
Arc<RwLock<T>> | Shared read-mostly state |
Arc<AtomicUsize> | Lock-free counter |
Arc<AtomicBool> | Atomic flag |
Arc<AtomicPtr<T>> | Lock-free pointer |
The Atomic Types
| Type | Operations |
|---|---|
AtomicUsize | load, store, fetch_add, fetch_sub |
AtomicBool | load, store, swap |
AtomicPtr<T> | load, store, swap |
AtomicI32 | load, store, fetch_add |
The Ordering Types
| Ordering | Use Case |
|---|---|
Relaxed | No ordering |
Acquire | Loads |
Release | Stores |
AcqRel | Both |
SeqCst | Total order |
Best Practices
✅ Do This:
// Use Arc for shared ownership across threads.
let data = Arc::new(vec![1, 2, 3]); // ✅
// Use Arc<Mutex<T>> for shared mutable state.
let counter = Arc::new(Mutex::new(0)); // ✅
// Use Arc<RwLock<T>> for read-mostly state.
let data = Arc::new(RwLock::new(vec![])); // ✅
// Use Arc<AtomicUsize> for a lock-free counter.
let counter = Arc::new(AtomicUsize::new(0)); // ✅
// Use Arc::clone to increment the count.
let b = Arc::clone(&a); // ✅
// Use Arc/Weak for thread-safe back references.
let weak = Arc::downgrade(&strong); // ✅
// Lock mutexes in a consistent order.
let _ga = a.lock(); let _gb = b.lock(); // ✅
// Handle the poisoning error.
let value = mutex.lock().unwrap(); // ✅
❌ Don’t Do This:
// Don't use Rc across threads.
let a = Rc::new(5);
thread::spawn(move || { *a }); // ❌ Not Send
// Don't use Arc<RefCell<T>> across threads.
let a = Arc::new(RefCell::new(0));
thread::spawn(move || { a.borrow_mut(); }); // ❌ Not Sync
// Don't hold a lock across an await.
let guard = mutex.lock().unwrap();
async_fn().await; // ❌ Deadlock risk
// Don't lock two mutexes in inconsistent order.
// Deadlock risk. // ❌
// Don't clone the inner value when an Arc clone works.
let b = (*a).clone(); // ❌ Clones the value
// Don't ignore the poisoning error.
let _ = mutex.lock(); // ⚠️ Hides the bug
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
Not Send | Rc across threads | Use Arc |
Not Sync | Arc<RefCell<T>> | Use Arc<Mutex<T>> |
| Deadlock | Inconsistent lock order | Lock in the same order |
| Poisoned mutex | Panic while holding the lock | Handle the error |
| Slow performance | Atomic overhead | Use Rc on one thread |
| Lost increment | Non-atomic count | Use Arc |
| Lock across await | Holding a lock in async | Use tokio::sync::Mutex |
Real-World Examples
1. Basic Arc
let a = Arc::new(5);
let b = Arc::clone(&a);
2. Shared Data
let data = Arc::new(vec![1, 2, 3]);
3. Arc<Mutex>
let counter = Arc::new(Mutex::new(0));
*counter.lock().unwrap() += 1;
4. Arc<RwLock>
let data = Arc::new(RwLock::new(vec![]));
let r = data.read().unwrap();
5. Atomic Counter
let counter = Arc::new(AtomicUsize::new(0));
counter.fetch_add(1, Ordering::SeqCst);
6. Atomic Flag
let flag = Arc::new(AtomicBool::new(false));
flag.store(true, Ordering::SeqCst);
7. Arc/Weak
let weak = Arc::downgrade(&strong);
8. Poisoning
match mutex.lock() {
Ok(g) => { /* ... */ }
Err(_) => { /* poisoned */ }
}
9. Consistent Lock Order
let _ga = a.lock();
let _gb = b.lock();
10. Thread Spawn
let data = Arc::clone(&data);
thread::spawn(move || { /* ... */ });
Visual
The Arc Structure
┌────────────────────────────────────────────────────────────┐
│ THE Arc STRUCTURE │
│ │
│ let a = Arc::new(5); │
│ let b = Arc::clone(&a); │
│ │
│ STACK HEAP │
│ ┌──────────┐ ┌──────────────────────┐ │
│ │ a: ptr ──┼──────────────────▶│ strong: 2 (atomic) │ │
│ ├──────────┤ │ weak: 0 │ │
│ │ b: ptr ──┼──────────────────▶│ value: 5 │ │
│ └──────────┘ └──────────────────────┘ │
│ │
│ The strong count is an AtomicUsize. │
│ Increments and decrements are atomic. │
│ The count is correct across threads. │
└────────────────────────────────────────────────────────────┘
The Arc<Mutex> Pattern
┌────────────────────────────────────────────────────────────┐
│ Arc<Mutex<T>> PATTERN │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Arc<T> │ │
│ │ Shared ownership across threads │ │
│ │ Atomic count │ │
│ └────────────────────┬─────────────────────────────┘ │
│ │ │
│ │ combined with │
│ v │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Mutex<T> │ │
│ │ Thread-safe mutation │ │
│ │ Lock-based │ │
│ │ Poisoning │ │
│ └────────────────────┬─────────────────────────────┘ │
│ │ │
│ v │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Arc<Mutex<T>> │ │
│ │ Shared mutable state │ │
│ │ Thread-safe │ │
│ │ Lock per access │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
The Atomic Count
┌────────────────────────────────────────────────────────────┐
│ THE ATOMIC COUNT │
│ │
│ Thread 1 Thread 2 │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Arc::clone(&a) │ │ Arc::clone(&a) │ │
│ │ │ │ │ │
│ │ CAS loop: │ │ CAS loop: │ │
│ │ read count │ │ read count │ │
│ │ compute +1 │ │ compute +1 │ │
│ │ compare_exchange │ │ compare_exchange │ │
│ └────────┬─────────┘ └────────┬─────────┘ │
│ │ │ │
│ └──────────┬─────────────┘ │
│ │ │
│ v │
│ ┌──────────────────────────────────────────────────┐ │
│ │ The compare_exchange retries if another thread │ │
│ │ modified the count. The final count is correct. │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
The Send and Sync Rules
┌────────────────────────────────────────────────────────────┐
│ SEND AND SYNC RULES │
│ │
│ Arc<T>: Send + Sync ⟺ T: Send + Sync │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Arc<i32> → Send + Sync ✅ │ │
│ │ Arc<Mutex<i32>> → Send + Sync ✅ │ │
│ │ Arc<RwLock<i32>> → Send + Sync ✅ │ │
│ │ Arc<RefCell<i32>> → NOT Sync ❌ │ │
│ │ Arc<Cell<i32>> → NOT Sync ❌ │ │
│ │ Arc<Rc<i32>> → NOT Send ❌ │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ The compiler checks the bounds at compile time. │
│ A violation is a compile error, not a runtime error. │
└────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
Arc<T> | Atomic reference-counted pointer |
| Count | AtomicUsize |
| Thread-safe | Yes, if T: Send + Sync |
Arc::new | Create a new Arc |
Arc::clone | Increment the count |
Arc::strong_count | The strong count |
Arc::try_unwrap | Extract if unique |
Arc<Mutex<T>> | Shared mutable state |
Arc<RwLock<T>> | Shared read-mostly state |
Arc<AtomicUsize> | Lock-free counter |
Arc<Weak<T>> | Thread-safe back reference |
Key takeaways:
Arc<T>is the thread-safe counterpart ofRc<T>. The count is anAtomicUsize, and the increment and decrement use compare-and-swap loops. The atomic operations make the count correct even when the value is shared across threads.Arc<T>isSendandSyncwhenTisSendandSync. The bounds are checked at compile time. AnArc<RefCell<T>>is notSync, becauseRefCell<T>is notSync, and the compiler rejects it.- The
Arc<Mutex<T>>combination is the standard for shared mutable state. TheArcprovides the shared ownership, and theMutexprovides the thread-safe mutation. TheArc<RwLock<T>>combination is the alternative for read-mostly workloads. - The atomic types provide lock-free mutation. The
AtomicUsize,AtomicBool, andAtomicPtr<T>types are the building blocks of lock-free code. They are faster than theMutexfor simple operations. - The
Orderingparameter controls the memory ordering. TheSeqCstordering is the safest and the slowest. TheRelaxedordering is the fastest and the least safe. TheAcquireandReleaseorderings are the middle ground. - The
Mutexis poisoned when a thread panics while holding the lock. Thelockmethod returns aResult, and the poisoning must be handled. Theunwrapis the common choice, because a poisoned mutex is a bug that should be surfaced. - The deadlock is the classic pitfall. Two threads that lock two mutexes in different orders deadlock. The fix is to lock in a consistent order, or to use a single lock, or to use a lock-free structure.
- The performance trade-off is the atomic overhead. An
Arc::cloneis slower than anRc::clonebecause of the atomic operations. The difference is small, but it is measurable in a tight loop. UseRcon a single thread andArcacross threads.
Remember: Arc<T> is the thread-safe reference-counted smart pointer. It uses an atomic count, which makes the sharing safe across threads. The Arc<Mutex<T>> combination is the standard for shared mutable state, and the Arc<RwLock<T>> combination is the alternative for read-mostly workloads. The atomic types provide the lock-free primitives, and the Ordering parameter controls the memory ordering. The Mutex is poisoned when a thread panics while holding the lock, and the deadlock is the classic pitfall. The result is shared ownership across threads without a data race, and a memory model that is safe, deterministic, and free of a garbage collector.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!