---
title: 'Atomic Reference Counting'
source: 'https://academia.sh/en/courses/ownership/atomic-reference-counting'
course: 'Ownership and the Memory Model'
language: en
updated: '2026-09-15T04:58:41+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Runs code showing that the same counting setup, built with Arc, produces exactly the same end record'
    - 'Explains that the difference between Rc and Arc lies not in behavior but in how the counter is updated'
    - 'Shows with an example that a value shared through Arc can only be read, never modified directly'
---

# Atomic Reference Counting

How the same counting mechanism, built with Arc, produces exactly the same end record — with the difference lying not in behavior but in how the counter is updated — is measured; which context it can be used in is not discussed here, since that is the subject of the Concurrency and Asynchronous Rust course. That shared data can only be read through an Arc, never modified directly, is also shown.

The previous lesson measured that reference counting worked with a counter updated like an
ordinary integer in a single context, that this counter rose with every clone and fell with
every end. This lesson measures a type that does the same counting job, but updates the
counter itself in a different way: `Arc<T>`. One limit needs stating up front: **in which
context** this difference is needed — several threads accessing the same value at the same
time — is not this lesson's subject, it is the subject of Concurrency and Asynchronous Rust.
Here only this is measured: does behavior change when the counter is updated atomically, or
does it not? What is measured is still a value's end — not how a running unit stops, the
question the Go curriculum's Concurrency course asks.

This limit's counterpart in the code is plain: no thread is started anywhere in this lesson.
All the runs below, like the previous lesson's, run start to finish within a single flow of
execution. This is not a gap, it is a scoping decision — even though `Arc`'s name evokes a
usage context, what this lesson measures is only the counter itself, not the environment it
is used in. This follows a rule this topic has already applied before: in the
Heap-Allocated Single Ownership lesson, why a piece of writing was rejected was described in
prose, and the accepted writing was run; here too, the context a type's name evokes is not
narrated, only what is measurable — the counter itself — is run.

## Same Counter, Different Update

`Arc` carries the same interface as `Rc`:

| Rc's counterpart | Arc's counterpart |
|---|---|
| `Rc::new` | `Arc::new` |
| `Rc::clone` | `Arc::clone` |
| `Rc::strong_count` | `Arc::strong_count` |

Each row's left and right sides are same-named calls on two different types doing the same
job: placing a value on the heap and setting the counter to one, producing a copy and
raising the counter, reading the counter's current value. The only difference is how the
increment and decrement operations are carried out. An ordinary increment consists of three
separate steps: the current value is read, one is added, the result is written back. An
**atomic** increment carries out these three steps as a single indivisible operation that
nothing else can interrupt in between. `Rc`'s counter uses an ordinary increment, `Arc`'s
counter uses an atomic increment — but both produce the same mathematical result: one rise
per clone, one fall per end. The same distinction holds for decrementing: a copy's end
lowers the counter by one through either an ordinary or an atomic decrement; whichever
method is used, the counter's final value is the same.

This lesson's measurement reruns the previous lesson's setup with `Arc`, in exactly the same
form.

In this lesson and the previous one, a copy is called not as `shared.clone()` but as
`Rc::clone(&shared)` or `Arc::clone(&shared)`. Both produce the same result — Rust allows a
method call to be written either as `value.clone()` or as `Type::clone(&value)` — but this
lesson prefers the second form throughout, because the first form can give the reader the
impression of "an operation that deep-copies the content". In fact what is cloned here is
only the counter and the access; the `Type::clone(&value)` form makes this clearer in the
source.

**SO8.** — the claim that "the same setup produces exactly the same counter sequence with
`Arc` too" is confirmed by the counter values printed in the sequence `1, 2, 3, 2` in the run
below — the exact same sequence as the previous lesson's `Rc` run.

```rust
// atomic_counter.rs — Arc does the same counting job atomically
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::Arc;

type Ledger = Rc<RefCell<Vec<String>>>;

struct Trace {
    name: String,
    ledger: Ledger,
}

impl Trace {
    fn new(name: &str, ledger: &Ledger) -> Trace {
        Trace {
            name: name.to_string(),
            ledger: Rc::clone(ledger),
        }
    }
}

impl Drop for Trace {
    fn drop(&mut self) {
        self.ledger.borrow_mut().push(self.name.clone());
    }
}

fn main() {
    let ledger: Ledger = Rc::new(RefCell::new(Vec::new()));
    {
        let shared: Arc<Trace> = Arc::new(Trace::new("atomic-shared", &ledger));
        println!("counter once built: {}", Arc::strong_count(&shared));

        let second = Arc::clone(&shared);
        println!("counter after one copy: {}", Arc::strong_count(&shared));

        {
            let _third = Arc::clone(&shared);
            println!("counter after two copies: {}", Arc::strong_count(&shared));
        }
        println!("counter after the inner scope ends: {}", Arc::strong_count(&shared));
        println!("second copy still readable: {}", second.name);
    }
    let record = ledger.borrow();
    println!("record count in the ledger: {}", record.len());
    println!("records: {}", record.join(", "));
}
```

```
counter once built: 1
counter after one copy: 2
counter after two copies: 3
counter after the inner scope ends: 2
second copy still readable: atomic-shared
record count in the ledger: 1
records: atomic-shared
```

The `Rc::clone(ledger)` call in this run is only used to share the `ledger` that holds the notes;
the sharing being measured is built with `Arc` on `shared`. The counter sequence — one, two,
three, then two — is exactly the same as the previous lesson's. When the inner scope closes,
`_third` ends, the counter returns to two, but the `Trace` value has not ended yet; when the
outer scope closes, the remaining two copies end as well and a single record appears in the
ledger. `Arc` does everything `Rc` does, in the same order, with the same result.

In the last line, reading `second.name` still gives the text `atomic-shared` — just as
`Rc::clone` did in the previous lesson, `Arc::clone` does not recall `Trace::new` either, it
only opens a second access path to the same value. The distinction built in the Cloning Is
Not Copying Data section holds here too: `Arc::clone`, despite the verb in its name,
multiplies not the data but the counter and the access to it. In this run too, `Trace::new`
is called only once; `second` and `_third` look at the same value, at the same heap
location, as `shared`.

## Same Setup, Same Result

When the two lessons' runs are placed side by side, no difference shows: the counter
sequence is the same, the ledger length is the same, the relationship between a copy ending and
the counter falling is the same. This is not a coincidence — `Arc` is a type carrying the
same contract as `Rc`; both enforce the rule that "the value ends when the counter hits
zero". The difference between them is **not in behavior**, but in the operation that updates
the counter itself: one does an ordinary increment/decrement, the other does it atomically.
In which context this difference gains meaning — several threads accessing the same counter
at the same time — is not taken up here; that question belongs to Concurrency and
Asynchronous Rust.

Two other properties measured in the Reference Counting lesson — that every value has its
own counter, and that the end depends on how many copies remain rather than which one — can
be tested for `Arc` as well. The run below builds two independent `Arc` values, drops one's
three copies in an order other than construction order, and shows the other value is
unaffected.

```rust
// arc_properties.rs — independent counters and order-independence hold for Arc too
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::Arc;

type Ledger = Rc<RefCell<Vec<String>>>;

struct Trace {
    name: String,
    ledger: Ledger,
}

impl Trace {
    fn new(name: &str, ledger: &Ledger) -> Trace {
        Trace {
            name: name.to_string(),
            ledger: Rc::clone(ledger),
        }
    }
}

impl Drop for Trace {
    fn drop(&mut self) {
        self.ledger.borrow_mut().push(self.name.clone());
    }
}

fn main() {
    let ledger: Ledger = Rc::new(RefCell::new(Vec::new()));
    let first: Arc<Trace> = Arc::new(Trace::new("first-atomic", &ledger));
    let second: Arc<Trace> = Arc::new(Trace::new("second-atomic", &ledger));

    let a = Arc::clone(&first);
    let b = Arc::clone(&first);
    println!(
        "counter after three copies — first: {}, second: {}",
        Arc::strong_count(&first),
        Arc::strong_count(&second)
    );

    drop(a);
    drop(first);
    println!(
        "ledger once two copies are dropped (should still be empty): {}",
        ledger.borrow().join(", ")
    );

    drop(b);
    println!(
        "ledger once the last copy is dropped too: {}",
        ledger.borrow().join(", ")
    );
    println!("second counter still unchanged: {}", Arc::strong_count(&second));

    drop(second);
    let record = ledger.borrow();
    println!("ledger once second is dropped too: {}", record.join(", "));
}
```

```
counter after three copies — first: 3, second: 1
ledger once two copies are dropped (should still be empty): 
ledger once the last copy is dropped too: first-atomic
second counter still unchanged: 1
ledger once second is dropped too: first-atomic, second-atomic
```

When `a` and the original `first` binding of the three copies are dropped, the ledger is still
empty — because `b` remains, `first-atomic` has not ended yet. Once `b` is dropped too, the
ledger fills. Meanwhile `second`'s counter never changes: the two values keep two independent
records. The result is exactly the same as what was seen for `Rc` in the Reference Counting
lesson — only the operation that updates the counter differs. The drop order here was again
chosen as `a`, `first`, `b`, outside the construction order (`first`, then `a`, then `b`);
the result still did not change, because what decides it is not which copy comes in the drop
order, but how many remain.

## Two Separate Types, a Visible Choice

Rust offering this as two separate types — `Rc` and `Arc` — rather than a single
configurable type is a design decision. Which form the counter is updated in is not a
runtime flag or a setup option, it is a **visible type choice in the source code**: whether
you build a value with `Rc::new` or with `Arc::new` makes readable from the source not who
may or may not read that value, but which operation its counter is updated with. This
repeats a pattern the course has seen many times before: a behavioral difference is tied not
to a runtime decision, but to a type difference. The two types can largely stand in for each
other — they work with the same `new`, `clone`, `strong_count` calls — but which one was
chosen stays in the source as a permanent piece of information about that value.

This arrangement takes the same shape as the distinction seen in the Ordered and Set
Structures lesson of the Rust Fundamentals course: a hash map and an ordered map do the same
basic job too, but the choice between them is not a runtime decision, it is a source
decision tied to which type is written. The choice between `Rc` and `Arc` fits the same
mold — both types give the same answer to the question "how many copies are there", the
only difference is how that answer is produced.

## The Atomic Counter Protects the Counter, Not the Data

Atomic updating protects only the **counter**; when it is the shared data itself at stake
rather than the counter, `Arc` gives no additional guarantee. A line that tries to write
directly to a field through an `Arc` does not compile: `Arc`, just like a cloned `Rc`, gives
only shared access to the value it holds — the same principle by which a reference (`&T`)
allows only reading. The accepted piece of writing only **reads** the same field.

This constraint is not actually unique to `Arc`; the same rule held for `Rc` in the previous
lesson too, it was just not spelled out there because the actual measurement was the
counter. The reason it is spelled out here is this: an atomic counter can create the
impression that "shared data can now safely be modified", because the word "atomic" evokes a
guarantee. The measurement corrects this impression — the guarantee is only for the
counter's own consistency, not for the content of the value the counter protects. Two copies
can read the same data at the same time, but neither can modify it on its own; the need to
modify it requires a separate tool.

**SO9.** — the claim that "several copies can read the same value but none of them can
modify it" is confirmed by the run below printing the same `value` field from both copies,
and containing no write line at all.

```rust
// shared_data_read_only.rs — only reading is possible through Arc
use std::sync::Arc;

struct Measurement {
    value: i64,
}

fn main() {
    let shared = Arc::new(Measurement { value: 42 });
    let copy = Arc::clone(&shared);

    println!("first read: {}", shared.value);
    println!("read from copy: {}", copy.value);
    println!("counter: {}", Arc::strong_count(&shared));
}
```

```
first read: 42
read from copy: 42
counter: 2
```

Both copies read the same `value` field and both give the same result, because there is in
fact a single `Measurement` value, and both copies access it. No line in this program writes
to the `value` field — that is not a coincidence, reading is the only operation `Arc`
allows. A line trying to set `value` to `43` would not compile; this rejection is not
additionally run in the program, because it is not placed into a source code block that does
not compile. The counter is also read in this run (`counter: 2`), but this line has no
relation to the `value` field — one tells how many copies there are, the other tells the
content of the value the copies look at; they are two independent, separate questions.

This example uses a plain `Measurement` struct instead of the `Trace`/`ledger` rig, because
what is measured is not an end record but access itself. The Heap-Allocated Single
Ownership lesson showed that reading `point.x` through a `Box` works as if the value sat
right there; here too, `shared.value` and `copy.value` use the same line form. The
difference is this: in `Box` this access was open to both reading and writing, because there
was a single owner. In `Arc` the same access is open only to reading, because ownership is
shared and a mechanism that tracks who writes to a shared value and when is not yet in
place.

When the three measurements come together, this emerges: at the counter level, `Arc` is a
strengthened form of `Rc`, but this strengthening never spreads to the shared data itself.
However robustly the counter is updated, whether the data is modifiable is a separate
question, and neither of these two types answers it yet.

Reading this topic's first three lessons together shows a progression. Heap-Allocated
Single Ownership established that a value can sit on the heap while its owner is still
singular. Reference Counting made that singularity extensible with a counter — ownership
was now a countable set. This lesson offered how that counter is updated as a variable
choice, but did not change the ownership and access model itself: however many copies there
are, none of them can modify the value. Across three lessons the container changed, the
counter changed, but the right to write to the value inside never arose — how that right is
gained is the topic's next and final question. What the three lessons share is this: in
each of them a value's lifetime is still readable from the source, none of them breaks this
determinism — only the form of ownership and the right of access widen, step by step.

## Summary

- `Arc<T>` carries the same interface as `Rc<T>` and enforces the same "the value ends when
  the counter hits zero" rule.
- The end record and counter sequence the two types produce are exactly the same — in both,
  the value's life ends **with its container**; the difference is not in behavior, but in
  the counter being updated atomically.
- In which context the atomic update is needed is not this lesson's subject; it belongs to
  Concurrency and Asynchronous Rust.
- Data shared through `Arc` can only be read; writing directly to a field does not compile.
- The atomic counter protects not the data but only itself; that shared data stays
  unmodifiable is a separate constraint.
- Every `Arc` value has its own counter, and the end depends not on which copy but on how
  many remain — this principle is the same as with `Rc`.

## Next Step

This lesson measured that a shared value can be read but not modified. The next question is
unavoidable: what happens if a shared value needs to be modified? The Variables and
Immutability lesson left a boundary open: a binding's immutability and a field inside it
being modifiable were separate questions, and that lesson only drew the boundary, it did not
close it. The next lesson — this topic's fourth — closes that boundary by measuring how
borrow checking can move from compile time to runtime, and what cost that move brings.
