Atomic values

atomic<T> stores one value whose supported operations are indivisible across native threads.

Primitive atomic storage

atomic<T> is a Core type, not an ownership wrapper or a library class. Primitive values use normal Xenon expressions: reading produces a T, and assignment performs an atomic write.

atomic<int> value = 0;

int current = value; // atomic read
value = 10;          // atomic write

Built-in integer and floating-point types support atomic reads and writes. bool supports reads and writes, enums use their integer representation, and raw pointers use the target's native pointer representation. Direct atomic operations use one safe, sequentially consistent ordering; source code does not select a memory order.

An atomic local may be declared before its initializer is available. Its first assignment initializes the atomic value; it does not read or destroy a previous value. Definite-assignment rules still reject a read before that first assignment.

Memory model

Atomic operations are the Core primitive for conflicting cross-thread access. Ordinary data races remain undefined behavior; see Memory model and data races.

Composite atomic values

atomic<T> also supports value structs, including nested structs and structs containing shared or weak ownership. A read observes one complete value, while assignment, exchange and compare-exchange replace the complete value without tearing, regardless of its size.

struct Snapshot
{
    public int left;
    public int right;
}

atomic<Snapshot> state = Snapshot { 0, 0 };
Snapshot next = Snapshot { 1, 1 };
Snapshot current = state;             // one consistent snapshot
bool changed = state : current --> next;

Composite atomic values may require additional synchronization storage, so sizeof(atomic<T>) may be greater than sizeof(T) and its alignment may also differ. Array layout and pointer arithmetic use the actual size and alignment of atomic<T>. Code must not assume that atomic<T> has the same native layout as its payload.

Struct equality for compare-exchange is evaluated field by field. Ownership-bearing fields retain the copied handles before the atomic operation completes. Destruction or ownership release for a displaced value occurs after the atomic replacement, so user destructor code does not execute as part of the indivisible update.

Atomic fields are not ordinary values

A struct that directly or recursively contains an atomic<...> field cannot be copied, moved, or swapped as an ordinary aggregate. These aggregate operations are invalid; access the atomic field explicitly instead.

Read-modify-write operators

Increment, decrement and supported compound assignments are a single atomic operation. They are not lowered as a separate load followed by a store.

value++;
--value;
value += 5;
value -= 2;
value |= mask;
value &= mask;
value ^= mask;

++, --, += and -= are available for numeric atomic values. |=, &= and ^= are available for integer atomic values. Other compound operators are rejected instead of silently producing a non-atomic update.

References and ownership

A reference may point to atomic storage. This is the normal way to let a function update an atomic owned elsewhere:

void Increment(atomic<int>& value)
{
    value++;
}

atomic<int>& and readonly atomic<int>& mean references to atomic storage. atomic<int&> is invalid. Atomicity protects operations on the stored value; it does not allocate memory, extend lifetimes, or make an unrelated object thread-safe.

Supported payloads

Atomic operations are defined for primitive, enum, raw pointer, unmanaged array handle, shared, weak and supported composite value types. Composite values containing references, unique ownership or nested atomic storage are invalid atomic payload types.

Native ABI

The native layout of atomic<T> is not a C ABI contract. extern and export reject direct atomic values, pointers or references to atomic storage, and pointers to structs that recursively contain atomic fields. Export ordinary functions that operate on private atomic storage instead.

Atomic ownership handles

atomic<shared<T>> atomically publishes a shared owner. Every read establishes its own strong ownership before a concurrent replacement can release the previous owner, so the returned handle remains valid independently.

atomic<shared<Config>> current = null;
shared<Config> replacement = new Config();

current = replacement;
shared<Config> snapshot = current;
current <-> replacement;

atomic<weak<T>> similarly protects publication and copying of a weak handle. Reading it first establishes independent weak ownership; the normal lock expression then performs the concurrency-safe weak-to-shared upgrade and cannot resurrect an object whose last strong owner has disappeared.

shared<Job> owner = new Job();
weak<Job> observer = owner;
atomic<weak<Job>> target = observer;

weak<Job> local = target;
shared<Job> strong = lock local;

Assignment, exchange and compare-exchange preserve the same ownership accounting. Releases of displaced handles happen after the atomic operation completes. atomic<unique<T>> is rejected because atomic reads would conflict with exclusive, move-only ownership.

Atomic arrays and array handles

atomic<T>[] is an array whose elements are independent atomic objects. Indexing selects one atomic element, so normal reads, assignments and read-modify-write operators apply to that element only. Array allocation and indexing use the actual size and alignment of atomic<T>, including any additional space required for composite atomic values.

atomic<int>[] counters = new atomic<int>[64];

counters[0]++;
counters[10] += 4;
delete(counters);

The same form supports composite and ownership-bearing elements, such as atomic<State>[] and atomic<shared<Job>>[]. Heap arrays remain unmanaged and must be deleted explicitly so live elements are destroyed. Stack allocation uses the corresponding atomic<T>[length] expression and performs normal element destruction when its scope ends.

atomic<T[]> has different semantics: the unmanaged array handle itself is atomic, while its elements are ordinary non-atomic values.

atomic<int[]> current;
int[] replacement = new int[16];

current = replacement;          // atomic handle write
int[] snapshot = current;   // atomic handle read
snapshot[5]++;               // ordinary element access
current <-> replacement;

An atomic unmanaged handle does not retain or free its allocation. The program must keep every concurrently readable array alive and must reclaim each heap allocation exactly once after it can no longer be observed. Stack array handles cannot be stored in atomic storage through initialization, assignment, exchange, or the desired operand of compare-exchange because that could let them escape their lifetime. The expected operand is only compared and does not escape.

Atomic raw pointers

atomic<T*> makes reads and replacements of the pointer value indivisible. It supports normal atomic reads and writes, exchange with a non-atomic pointer, and compare-exchange including null.

atomic<Job*> current = null;
Job* replacement = FindJob();

current = replacement;
Job* snapshot = current;
bool installed = current : null --> replacement;
current <-> replacement;

Atomicity applies only to the pointer bits. It does not retain, destroy or otherwise manage the pointee, and it does not make the pointed-to object thread-safe. The program remains responsible for ensuring that a loaded pointer stays valid while it is used.

Pointer arithmetic is deliberately not provided directly on atomic<T*>. Read a snapshot, perform ordinary pointer arithmetic where valid, then use compare-exchange if the update must be conditional.

Atomic exchange

If exactly one operand of <-> is atomic, Xenon performs one indivisible exchange. The ordinary operand receives the previous atomic value.

atomic<int> current = 10;
int replacement = 20;

current <-> replacement;
// current == 20, replacement == 10

The symmetric form replacement <-> current has the same semantics. Swapping two atomic locations is rejected because it would imply a multi-location atomic transaction that the operator does not provide.

Compare-exchange

value : expected --> desired conditionally replaces one atomic value. It returns true only when the stored value equals expected and is replaced with desired; otherwise the stored value is unchanged and the expression returns false.

atomic<int> state = 0;
int expected = 0;
int desired = 1;

bool won = state : expected --> desired;
// expected and desired are still 0 and 1

The target must be a writable atomic<T> storage location, and both value expressions must be compatible with T. The target address, expected, and desired are each evaluated exactly once, from left to right. Unlike APIs that take an expected value by reference, Xenon never overwrites expected on failure.

Comparison follows built-in Xenon equality for T; user-defined operators do not participate in atomic compare-exchange. For float and double, +0.0 equals -0.0, while every NaN comparison is unequal, including comparison with the same NaN bit pattern.

The entire : ... --> ... form is one expression and cannot be chained. Primitive operations use the built-in sequentially consistent ordering; composite values provide the same indivisible replacement semantics. No memory-order or weak/strong switch is exposed in source code.