std.atomic
std.atomic provides lock-free atomic access to a single value shared between threads: the Atomic<T> cell and the MemoryOrder enum that selects how each operation orders surrounding memory. Atomic operations are race-free by construction — concurrent access to the same Atomic<T> through its methods is never a data race — and MemoryOrder controls only the visibility of other memory relative to the operation. For the concurrency model these fit into, see Concurrency And Parallelism; for the lock-based escape hatch, see std.mutex.
Quick Example
import std.atomic;
fn main() {
let counter = atomic.new<i32>(0);
counter.store(42);
assert counter.load() == 42;
// Read-modify-write ops return the previous value.
let prev = counter.swap(7);
assert prev == 42;
assert counter.fetch_add(3) == 7;
assert counter.load(.Relaxed) == 10;
// Compare-and-swap yields (observed, succeeded).
let (old, ok) = counter.compare_exchange(10, 20);
assert ok;
assert old == 10;
assert counter.load() == 20;
}Contract
atomic.new<T>(value)mints an^Atomic<T>owningvalue.Atomic<T>is notCopy— its address is its synchronization identity — so it is moved, not copied.- Every method takes
selfby a shared borrow:store,swap, and thefetch_*family mutate through a shared reference (interior mutability), so a plainlet cell = ...binding is enough to mutate it. The mutation is atomic, hence race-free. - Each operation takes a trailing
order: MemoryOrder = .SeqCst. The default is sequential consistency; pass a weaker ordering only when you have reasoned about it. - The ordering argument selects one hardware ordering. When it is a compile-time constant — the usual case, including the
.SeqCstdefault — codegen folds it to a single atomic instruction with no runtime branch.
Memory Ordering
MemoryOrder maps one-to-one onto the underlying hardware/LLVM atomic model. An operation's ordering constrains how the issuing thread's other memory accesses become visible to threads that observe this atomic:
Relaxed— atomicity only. The atomic object still has a single total modification order all threads agree on, but no ordering is imposed on any other memory location. Use for counters read only for statistics, or flags whose payload is synchronized separately.Acquire(loads, and the load half of a read-modify-write) — no access later in program order may move before it, and the thread observes every write the producer released before the value it reads. The reader side of a synchronizes-with edge.Release(stores, and the store half of a read-modify-write) — no access earlier in program order may move after it, and all such prior writes become visible to any thread that later acquires this value. The producer side.AcqRel(read-modify-write only) —Acquireon the read andReleaseon the write.SeqCst— acquire/release as applicable, plus a single global total order over allSeqCstoperations. The default.
A Release store on an atomic that writes v synchronizes-with an Acquire load that reads v; combined with program order this forms happens-before, which is how an atomic publishes non-atomic data (write the data, then a Release flag; Acquire the flag, then read the data).
A data race — two accesses to the same non-atomic location from different threads, at least one a write, not ordered by happens-before — is undefined behavior. Access to the same Atomic<T> through its operations is never a race, even under Relaxed. The stored value is private to the cell and only reachable through the atomic operations, so a race requires deliberate unsafe pointer laundering.
MemoryOrder constrains atomic operations and ordinary shared-memory accesses; it does not order std.mem.volatile operations or device/MMIO effects. Volatile access likewise creates no atomic happens-before edge. A target-specific device API must provide any required fence, cache/DMA maintenance, bus ordering, or completion guarantee explicitly.
Which Types Are Atomic
Atomic<T> admits only types the hardware can operate on atomically — there is no silent lock fallback:
- Integers: the widths
i8..i64,u8..u64,usize, andisizeget the full API, including thefetch_*arithmetic and bitwise operations. - Other scalars:
bool, raw pointers, and int-backed enums getload/store/swap/compare_exchange[_weak]only — thefetch_*operations (arithmetic and bitwise) are integer-only. AtomicRepresentableaggregates (see below): small, padding-free structs/enums, restricted toload/store/swap/compare_exchange— no arithmetic.
A T that is not lock-free on the target is a compile error that points you at std.mutex. The maximum atomic width is the target's native one (8 bytes everywhere by default); 16-byte atomics require an opt-in hardware feature (cx16 on x86-64, lse2 on AArch64), otherwise a 16-byte T is rejected rather than lowered to a lock.
AtomicRepresentable
A struct or enum opts into atomic access by declaring : AtomicRepresentable. The value's own bytes are its representation, so it must be padding-free and fit a native atomic width (a power-of-two byte size up to the target maximum); both are checked where it is used as Atomic<T>. This is exactly the pointer-plus-generation (ABA) pattern.
import std.atomic;
struct Tagged: AtomicRepresentable, Copy {
let index: u32,
let generation: u32,
}
fn main() {
let cell = atomic.new<Tagged>(Tagged { index: 1, generation: 0 });
let (old, ok) = cell.compare_exchange(
Tagged { index: 1, generation: 0 },
Tagged { index: 2, generation: 1 });
assert ok;
assert old.index == 1;
assert cell.load().generation == 1;
}Representable aggregates get load, store, swap, compare_exchange, and compare_exchange_weak only; the arithmetic fetch_* operations remain integer-only.
compare_exchange
compare_exchange(expected, desired, success, failure) atomically sets the value to desired when it currently equals expected, returning (old, true); otherwise it leaves the value unchanged and returns (current, false). It takes two orderings: success orders the read-modify-write on a match, and failure orders the pure load on a mismatch. failure describes a load, so a Release/AcqRel failure ordering is meaningless and is coerced to its load equivalent; conventionally failure is no stronger than success.
compare_exchange_weak may fail spuriously even when the value matches — prefer it inside a retry loop, where a spurious failure just loops again and generates tighter code on load-linked/store-conditional targets (AArch64):
import std.atomic;
fn main() {
let cell = atomic.new<i32>(0);
// Increment via a weak-CAS loop.
var cur = cell.load(.Relaxed);
var done = false;
while !done {
let (observed, ok) = cell.compare_exchange_weak(cur, cur + 1, .AcqRel, .Relaxed);
if ok {
done = true;
} else {
cur = observed;
}
}
assert cell.load() == 1;
}Ownership, Allocation, And Errors
Atomic<T> owns its T inline and is move-only (not Copy). The operations do not allocate and do not throw. Floating-point atomics are not provided in this release; use an integer or a representable wrapper.
Related Modules
Use std.mutex for values that are not lock-free-atomic-eligible, and std.channel for message passing. See Concurrency And Parallelism for how atomics fit the runtime and memory model, and Standard Library Inventory for the full exported surface.
API Reference
Types
| Type | Kind | Description |
|---|---|---|
MemoryOrder | enum | Ordering selector with variants Relaxed, Acquire, Release, AcqRel, SeqCst. Payload-less, copyable. |
Atomic<T> | struct | Lock-free atomic cell over a lock-free-eligible T. Move-only. |
AtomicRepresentable | interface | Marker: a padding-free, native-width struct/enum whose bytes may be atomically loaded/stored/swapped/compare-exchanged. |
Functions
| Function | Signature | Returns | Description |
|---|---|---|---|
new | new<T>(value: ^T) -> ^Atomic<T> | ^Atomic<T> | Construct an atomic cell owning value. |
Atomic Methods
| Method | Signature | Returns | Description |
|---|---|---|---|
load | load(self, order: MemoryOrder = .SeqCst) -> T | T | Atomically read the current value. |
store | store(self, value: T, order: MemoryOrder = .SeqCst) | — | Atomically replace the value. |
swap | swap(self, value: T, order: MemoryOrder = .SeqCst) -> T | T | Replace the value, returning the previous one. |
fetch_add | fetch_add(self, value: T, order: MemoryOrder = .SeqCst) -> T | T | Integer add, returning the previous value. |
fetch_sub | fetch_sub(self, value: T, order: MemoryOrder = .SeqCst) -> T | T | Integer subtract, returning the previous value. |
fetch_and | fetch_and(self, value: T, order: MemoryOrder = .SeqCst) -> T | T | Bitwise AND, returning the previous value. |
fetch_or | fetch_or(self, value: T, order: MemoryOrder = .SeqCst) -> T | T | Bitwise OR, returning the previous value. |
fetch_xor | fetch_xor(self, value: T, order: MemoryOrder = .SeqCst) -> T | T | Bitwise XOR, returning the previous value. |
fetch_nand | fetch_nand(self, value: T, order: MemoryOrder = .SeqCst) -> T | T | Bitwise NAND, returning the previous value. |
compare_exchange | compare_exchange(self, expected: T, desired: T, success: MemoryOrder = .SeqCst, failure: MemoryOrder = .SeqCst) -> (T, bool) | (T, bool) | Strong CAS: (old, true) on a match, else (current, false). |
compare_exchange_weak | compare_exchange_weak(self, expected: T, desired: T, success: MemoryOrder = .SeqCst, failure: MemoryOrder = .SeqCst) -> (T, bool) | (T, bool) | Weak CAS; may fail spuriously. Prefer in a retry loop. |
Arithmetic and bitwise fetch_* are integer-only; load/store/swap/ compare_exchange[_weak] also apply to raw pointers, int-backed enums, and AtomicRepresentable aggregates.
Status
std.atomic is the current public atomic API: MemoryOrder, Atomic<T> over the native lock-free scalar set plus AtomicRepresentable aggregates, with a target-width gate and no silent lock fallback. Floating-point atomics are not part of this release, and neither is a cross-thread sharing path: this release provides no way to share an Atomic across concurrent tasks (by shared borrow or multi-owner handle) and does not yet move an owned Atomic across a worker boundary. Today the type provides the memory-ordering model and interior mutability; for communication between tasks, use a channel. For the full exported surface, see Standard Library Inventory.