The Atomic Reference Counter (Arc) type is a smart pointer that lets you share immutable data across threads in a thread-safe way. I couldn’t find any good articles explaining how it works, so that’s what I’ll attempt to do here. The first section is an introduction on how and why to use Arc; if you already know this and just want to know how it actually works, skip to the second section titled; “How does it work”.
Why would you need to use Arc?
The reason for needing the Arc type when attempting to share data across threads is to ensure that the lifetime of the type that is being shared, lives as long as the longest lasting thread. Consider this example;
This code will not compile; We will get an error saying that the reference to foo outlives foo itself. This is because foo gets dropped at the end of the main function and then the dropped value will try to be accessed 20 milliseconds later in the spawned thread. This is where Arc comes in. The atomic reference counter essentially makes sure that the foo type will not be dropped until all references to it have ceased — So that even after the main function resolves, foo will still exist. Now consider this example;
In this example we can now, reference foo in the thread and also access it’s value after the thread has been spawned.
How does it work?
Now you know how to use Arc, let us talk about how it works. When you call let foo = Arc::new(vec!), you are creating the value vec!, an atomic counter with a value of 1 and storing both values in the same location on the heap. A pointer to this data on the heap is then stored in foo. So, foo now consists of a pointer to an object which contains vec! and atomic_counter.
When you call let bar = Arc::clone(&foo), you are taking a reference to foo, dereferencing foo (which is a pointer to the data on the heap remember), then going to the address that foo points to, finding the values there (which is vec! and the atomic_counter), incrementing the atomic counter by one and then storing the address that points to the vec! in bar.
When foo or bar fall out of scope, and Arc::drop() is then consequently called, the atomic_counter is decremented by one. If Arc::drop() finds that the atomic counter is equal to 0, then the data that it points to on the heap (vec! and atomic_counter) are cleared up and erased from the heap.
An atomic counter is a type that lets you mutate and increment its value in a thread safe way; Each operation on an atomic type is completed fully before other operations are allowed; Hence, the name atomic (which means indivisible).
It is important to note that Arc can only contain immutable data. This is because Arc cannot guarantee safety from data-races, should two threads try to mutate the value contained within at the same time. If you wish to mutate data, you should encapsulate a Mutex guard inside the Arc type.
So why do these things make Arc thread safe?
Arc is thread safe because it ensures the compiler that the reference to the data will live at least as long as the data itself. This is because every time you create a new reference to the data on the heap, the atomic counter is incremented by one, and the data is only dropped if the atomic counter is equal to zero (with decrements happening each time a reference falls out of scope) – The difference between Arc and a normal Rc (reference counter) is just the atomic counter.
So what is the point of Rc and why not use Arc for everything?
The reason, is that an atomic counter is an expensive variable type, whereas a normal usize type is not. Not only does an atomic counter take up more memory in the actual program itself, each operation also takes longer because it has to apportion resources to maintain a queue for each call to read/write to itself so as to ensure atomicity.