The Arc<T> stands for Atomic Reference Counted smart pointer. It is a powerful tool that allows multiple threads to share ownership of the same data safely.
Rc<T> is only safe for single-threaded programs, but Arc<T> is a multi-threaded version and uses atomic operations to ensure thread safety.
It allows multiple threads to share ownership of the same data, ensuring that the data is only deallocated when the last reference is dropped.
The word “atomic” in Arc means that operations like increasing or decreasing the reference count are done safely and completely, even if multiple threads try to do them at the same time.
In normal situations, if two threads update the same number at once, the value can become incorrect. But with atomic operations, Rust makes sure that these updates happen one by one, without any conflict.
How to Create and Use an Arc Pointer?
The Arc<T> is very similar to using Rc<T>, but it’s built for multi-threaded environments. We can create it with Arc::new() and then use Arc::clone() whenever we want to share ownership of the same data with another thread.
Example: Using Arc with Multiple Threads
use std::sync::Arc;
use std::thread;
fn main() {
// Step 1: Create an Arc pointer holding a string on the heap
let message = Arc::new(String::from("Hello from Arc!"));
// Step 2: Create multiple threads and share the same data
let mut handles = vec![];
for i in 0..3 {
let shared_message = Arc::clone(&message); // Share ownership
let handle = thread::spawn(move || {
println!("Thread {} received: {}", i, shared_message);
});
handles.push(handle);
}
// Step 3: Wait for all threads to complete
for handle in handles {
handle.join().unwrap();
}
}
Output:
Thread 0 says: Hello, Arc Pointer!
Thread 1 says: Hello, Arc Pointer!
Thread 2 says: Hello, Arc Pointer!
Explanation of above code:
- Arc::new() creates an Arc pointer that stores your data on the heap and allows it to be shared between threads.
- Arc::clone() instead of copying the data, this shares ownership by increasing the reference count atomically.
- thread::spawn() creates a new thread. Because we move the cloned Arc into the thread, that thread can safely read the shared data.
Essentials Methods in Arc<T> Pointer
These methods help you create, clone, and inspect the shared data safely and easily. Let’s look at them one by one in a simple way:
a) Arc::new(value)
This method is used to create a new Arc pointer that owns the given data. The data is stored on the heap, and you get a smart pointer that can be shared safely between threads. For example:
use std::sync::Arc;
fn main() {
let shared_data = Arc::new(42);
println!("Value: {}", shared_data);
}
- Here, shared_data is an Arc smart pointer that holds the integer 42.
- You can now clone this Arc to share it across multiple threads.
b) Arc::clone(&arc): Arc::clone() creates another reference to the same data. Each clone increases the reference count, which allows multiple threads to safely access the same memory.
For example:
use std::sync::Arc;
fn main() {
let data = Arc::new("Rust is safe!");
let another_ref = Arc::clone(&data);
println!("Original: {}", data);
println!("Cloned: {}", another_ref);
}
In this code:
- Both data and another_ref now point to the same data.
- The data is not duplicated; only the reference count is increased.
c) Arc::strong_count(&arc)
This method returns the number of strong references currently pointing to the data. It’s useful for debugging or verifying how many threads or variables are sharing the data. For example:
use std::sync::Arc;
fn main() {
let value = Arc::new(100);
let clone1 = Arc::clone(&value);
let clone2 = Arc::clone(&value);
println!("Reference Count: {}", Arc::strong_count(&value));
}
- The reference count will be 3, one for the original Arc and two for the clones.
- Once a clone is dropped, the count automatically decreases.
d) Arc::weak_count(&arc) – This method returns the number of weak references associated with the data. Weak references are non-owning and do not increase the strong reference count.
For example:
use std::sync::{Arc, Weak};
fn main() {
let data = Arc::new(10);
let weak_ref: Weak<i32> = Arc::downgrade(&data);
println!("Strong Count: {}", Arc::strong_count(&data));
println!("Weak Count: {}", Arc::weak_count(&data));
}
- Arc::downgrade() creates a weak reference that doesn’t affect the strong count.
- weak_count helps you see how many weak references exist.
Sharing Mutable Data with Arc
An Arc<T> (Atomic Reference Counted pointer) only gives you read-only access to the data it shares.
But in real-world multithreading, we need to modify shared data (for example, updating a counter or appending to a list).
However, directly mutating shared memory from multiple threads is not safe; it can lead to data races. Rust allows us to combine Arc<T> and Mutex<T> to lock and safely mutate the shared data one thread at a time.
Example: Using Arc with Mutex
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
// Create shared mutable data wrapped with Arc + Mutex
let counter = Arc::new(Mutex::new(0));
// Launch 5 threads to update the shared counter
let handles: Vec<_> = (0..5).map(|_| {
let counter_clone = Arc::clone(&counter); // share ownership
thread::spawn(move || {
// Lock the Mutex before accessing data
let mut num = counter_clone.lock().unwrap();
*num += 1; // Safely mutate the data
})
}).collect();
// Wait for all threads to finish
for handle in handles {
handle.join().unwrap();
}
// Access the final value
println!("Final counter value: {}", *counter.lock().unwrap());
}
Output:
Final counter value: 5
In this code:
- We wrap 0 (our shared counter) inside a Mutex (for safe mutation) and then inside an Arc (for thread-safe sharing).
Preventing Memory Leaks with Weak<T>
Rust gives us Weak<T> a non-owning reference to data managed by an Arc. It does not increase the reference count, so even if two objects refer to each other, they won’t keep each other alive forever.
Example: Using Weak References with Arc
use std::sync::{Arc, Weak};
use std::cell::RefCell;
struct Node {
value: i32,
parent: RefCell<Weak<Node>>, // Weak reference to parent
children: RefCell<Vec<Arc<Node>>>, // Strong references to children
}
fn main() {
// Create a parent node
let parent = Arc::new(Node {
value: 100,
parent: RefCell::new(Weak::new()), // no parent yet
children: RefCell::new(Vec::new()),
});
// Create a child node
let child = Arc::new(Node {
value: 200,
// Downgrade the parent Arc to a Weak reference
parent: RefCell::new(Arc::downgrade(&parent)),
children: RefCell::new(Vec::new()),
});
// Add the child to the parent's children list
parent.children.borrow_mut().push(Arc::clone(&child));
// Accessing values
println!("Parent Node Value: {}", parent.value);
println!("Child Node Value: {}", child.value);
// Try upgrading Weak reference to Arc (if parent still exists)
if let Some(upgraded_parent) = child.parent.borrow().upgrade() {
println!("Child's Parent Value: {}", upgraded_parent.value);
} else {
println!("Parent has been dropped!");
}
}
Output:
Parent Node Value: 100
Child Node Value: 200
Child's Parent Value: 100
In this code:
- We use Arc to share ownership between parents and children safely.
- The child uses a Weak pointer to refer back to the parent. This breaks the ownership cycle because the parent is not strongly owned by the child.
When to Use Arc<T>
1) Threaded Programs: If your program creates multiple threads that all need to read or access the same piece of data, Arc<T> is the perfect solution.
It ensures that the data is not dropped while any thread is still using it, and it automatically handles reference counting in a thread-safe way.
2) Shared Immutable Data: if your data is not going to change, only different threads need to read or access it, then Arc<T> alone is enough to safely share that data.
Immutable data (data that doesn’t change) is automatically safe to use in multiple threads. Rust guarantees that no thread can accidentally change it, so you don’t need extra tools like Mutex for locking or synchronization.
3) Prevent Data Races: Arc<T> by itself provides shared ownership with read-only access. If you also need to mutate the data across threads, you must combine it with a synchronization primitive like Mutex<T>.
This combination (Arc<Mutex<T>>) allows multiple threads to safely read and write shared data one at a time.