In this lesson, you will learn why choosing between Rc<T> and Arc<T> involves a trade-off between performance and thread safety.
When we need shared ownership of data, Rust gives us two primary tools: Rc<T> (Reference Counted) and Arc<T> (Atomic Reference Counted). Both keep track of how many pointers exist to a piece of data, freeing that data only when the count reaches zero. However, they handle these counts very differently.
The Cost of Synchronization
The Rc<T> pointer is designed for single-threaded scenarios. Because it assumes no other thread will be modifying the reference count at the same time, it updates the count using standard, non-atomic CPU instructions. These are extremely fast, essentially just a simple increment or decrement operation in memory.
In contrast, Arc<T> is designed for thread safety. When you clone an Arc, the reference count must be incremented across potential concurrent access from multiple threads. To prevent a "race condition" (where two threads try to update the counter simultaneously and corrupt it), Arc uses atomic operations.
An atomic operation forces the CPU to synchronize its cache and ensure that the increment is "all or nothing" and visible to all cores. This synchronization creates a bottleneck known as a memory barrier.
Why not use Arc everywhere?
You might wonder why we don't just use Arc<T> for everything. The answer is overhead:
- CPU Cycles: Atomic instructions are significantly slower than standard integer increments.
- Memory Barriers:
Arcoperations can prevent the CPU from reordering instructions for optimization, which impacts the overall pipeline speed of your code.
If you are working in a single-threaded loop, Rc<T> allows your code to run at the maximum possible speed. If you use Arc<T> in that same loop, you pay a "synchronization tax" for safety features you aren't actually using.
use std::rc::Rc;
use std::sync::Arc;
// Fast: Non-atomic increment (local thread only)
let local = Rc::new(10);
// Slower: Atomic increment (thread-safe, cache-synchronized)
let shared = Arc::new(10);
Key Takeaway: Use Rc<T> for maximum performance in single-threaded code, and only upgrade to Arc<T> when you specifically need to share data across thread boundaries.