Welcome to the Arc<T> tutorial! Today, we're going to dive into Atomic Reference Count (Arc) in Rust. This is a powerful tool for managing shared ownership, and it's essential for understanding Rust's unique approach to memory management.
šÆ Key Learning Objectives
Before we delve into Arc, let's refresh our understanding of Rust's ownership model and shared ownership.
Rust has a strict borrowing and lifetime system to manage memory safely. Each value in Rust has a variable that's called its owner. During the lifetime of an owner, the value it owns is also alive. When the owner goes out of scope, the value it owns is dropped, and its memory is deallocated.
Shared ownership, on the other hand, refers to a situation where multiple variables own the same data. This may seem contradictory to Rust's ownership model, but Rust provides a solution for this through smart pointers.
Arc (Atomic Reference Count) is a smart pointer that allows multiple owners to share a single resource. Arc's reference count is guaranteed to be atomically incremented and decremented, ensuring thread-safe shared ownership.
Let's create our first Arc!
use std::sync::Arc;
let data = 10;
let arc_data = Arc::new(data);In the example above, we've created an Arc called arc_data that contains the integer value 10.
To access the data inside an Arc, we can use the .clone() method to get a mutable reference to the data. Cloning an Arc increments its reference count.
let mutable_ref = arc_data.clone();Now we have mutable_ref, a mutable reference to the data inside arc_data. But what happens when we drop arc_data? Let's find out.
When an Arc's owner goes out of scope, its reference count is decremented. If the reference count reaches zero, the Arc's data is dropped, and its memory is deallocated.
{
let data_ref = arc_data.clone();
// Some code here
drop(data_ref);
}In the example above, we've cloned arc_data and stored it in data_ref. Once the {} block is closed, data_ref goes out of scope, and its reference count is decremented. However, since there's still another reference to the data inside arc_data, the data isn't dropped yet.
Arc has two derived types: Arc<T>, which we've already seen, and Mutex<T>. A Mutex<T> is used to synchronize access to mutable data between multiple threads, ensuring thread safety.
To create a Mutex, we need to import the std::sync::Mutex module and wrap our data inside a new Mutex object.
use std::sync::{Arc, Mutex};
let data = Arc::new(Mutex::new(10));Now we have a data variable that contains an Arc with a Mutex, allowing us to safely share mutable data between threads.
To access the data inside a Mutex, we need to use the lock() method. The lock() method acquires exclusive access to the Mutex and returns a MutexGuard object, which allows us to access the data inside the Mutex.
let mut data_ref = data.clone();
let lock = data_ref.clone().lock().unwrap();
let value = *lock.get();In the example above, we've cloned data_ref twice. The first clone is to get a MutexGuard to access the data inside the Mutex. The second clone is to safely pass the Mutex to another thread if needed.
Now that we understand Arc and Mutex, let's put our knowledge to use in a real-world example. We'll create a simple concurrent counter using Arc and Mutex.
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for i in 1..=10 {
let counter = counter.clone();
let handle = thread::spawn(move || {
for _ in 0..100 {
let mut num = counter.clone();
let mut lock = num.lock().unwrap();
*lock.get() += 1;
*lock.get();
thread::sleep(Duration::from_millis(10));
}
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}In this example, we've created a shared counter using Arc and Mutex. We've spawned ten threads, each incrementing the counter 100 times with a 10ms delay between increments. When all threads have finished executing, the counter should have a value of 1000.