once-arc
A lock-free, thread-safe container that can be atomically initialized once with an Arc<T>.
Think of it as Atomic<Option<Arc<T>>> — but with a critical restriction:
the value can only be set once. In return, reads are extremely fast:
a single atomic read, no reference count manipulation, no locking.
Usage
Quick start
OnceArc — low-level, lock-free
use Arc;
use Ordering;
use OnceArc;
let slot: = new;
// Set it once
slot.store.unwrap;
// get() returns &T — a single atomic load, no refcount overhead
assert_eq!;
// load() clones the Arc when you need ownership
let arc = slot.load.unwrap;
assert_eq!;
// Second store fails, returning the value back
let err = slot.store.unwrap_err;
assert_eq!;
InitOnceArc — Mutex protected initialization
use Arc;
use Ordering;
use InitOnceArc;
let cell: = new;
// First call runs the closure
cell.init.unwrap;
// The value is already set, so the closure is not run
cell.init.unwrap;
assert_eq!;
Multiple threads can race to call store/init/try_init; exactly one will run the
closure, and the rest will block briefly on the mutex.
API overview
OnceArc<T>
| Method | Returns | Cost |
|---|---|---|
get(Ordering) |
Option<&T> |
Single atomic load |
load(Ordering) |
Option<Arc<T>> |
Atomic load + Arc::clone |
store(Arc<T>, Ordering) |
Result<(), Arc<T>> |
One CAS |
is_set(Ordering) |
bool |
Single atomic load |
into_inner() |
Option<Arc<T>> |
No atomic ops (consumes self) |
get_mut() |
Option<&mut T> |
No atomic ops (exclusive ref) |
InitOnceArc<T>
| Method | Returns | Cost |
|---|---|---|
get(Ordering) |
Option<&T> |
Single atomic load |
load(Ordering) |
Option<Arc<T>> |
Atomic load + Arc::clone |
store(Arc<T>) |
Result<(), Result<Arc<T>, PoisonError>> |
Mutex lock + CAS |
init(f) |
Result<bool, PoisonError> |
Mutex lock + closure (once) |
try_init(f) |
Result<bool, Result<E, PoisonError>> |
Mutex lock + fallible closure (once) |
is_set(Ordering) |
bool |
Single atomic load |
into_inner() |
Option<Arc<T>> |
No atomic ops (consumes self) |
get_mut() |
Option<&mut T> |
No atomic ops (exclusive ref) |
Why not just use OnceLock<Arc<T>>?
OnceLock stores the value inline. get() returns &Arc<T>, so
callers must go through two pointer indirections to reach T, and
cloning requires an Arc::clone. With OnceArc, the atomic
is the Arc's pointer — get() returns &T directly with a single
atomic load and zero indirection beyond the pointer itself.
Why is a general Atomic<Arc<T>> so hard?
A general-purpose Atomic<Arc<T>> that supports multiple stores is
deceptively difficult to implement correctly. Here's why:
The fundamental race condition
Consider a naive load implementation for Atomic<Arc<T>>:
- Thread A atomically reads the raw pointer from the atomic slot.
- Thread B atomically swaps in a new
Arc, then drops the old one. - The old
Arc's reference count hits zero — the data is freed. - Thread A tries to increment the reference count of the now-freed pointer. Use-after-free.
The core problem: between reading the pointer and incrementing the reference count, another thread can remove and destroy the pointed-to data. There is no way to make "read pointer" and "increment refcount" into a single atomic operation on standard hardware.
Solutions exist, but they're expensive
Real implementations of atomic shared pointers (like C++20's
std::atomic<std::shared_ptr<T>> or Rust's arc-swap crate) use
techniques like:
-
Hazard pointers — readers publish which pointers they're looking at, and writers defer freeing memory until no reader holds a hazard on it. Adds per-thread bookkeeping and slows down both loads and stores.
-
Epoch-based reclamation — threads enter/exit "epochs" and memory is only freed once all threads have advanced past the epoch where the pointer was removed. Requires cooperative epoch advancement.
-
Split reference counts — maintain both a "global" and "local" reference count, using the global count to defer destruction. Loads must still atomically modify the global count.
All of these approaches add overhead to loads — exactly the operation that's typically on the hot path.
How initialize-once sidesteps all of this
OnceArc avoids every one of these problems with a single
rule: the pointer, once written, never changes.
- No use-after-free: the
Arcis alive from the moment ofstore()until theOnceArcis dropped. No thread can remove it in between. - No ABA problem: the value transitions from null to a pointer exactly once. No pointer is ever reused.
- No deferred reclamation: since the pointer is never swapped out, there's nothing to reclaim while readers exist.
- No refcount on
get():get()returns&Ttied to the lifetime of&self. Since the data can't be freed while a shared reference to the container exists, this is sound without touching the reference count.
The result: get() compiles down to a single atomic load instruction
(which on x86 is just a plain mov), making it effectively zero-cost.