# threads.atomic

> A module of threads's API.

Atomics that compile on Mojo 1.2.0 *and* on current nightly.

Everything here operates on a **naturally aligned 64-bit cell at an address you
own**. That is on purpose: atomics are only interesting between threads, and
threads in this library share state through a raw context pointer, so the
useful shape is a view over somebody else's memory rather than a value you hold.
`AtomicCounter` and `AtomicFlag` are both such views — copying one copies the
view, not the cell. The seven free functions below are that view expressed as
`std.atomic` calls against a `Cell` the caller's pointer is bitcast to; a `Cell`
is laid out exactly like the `Int64` it aliases, which the `comptime assert`s in
`_cell` pin down so that a future stdlib change to `Atomic` fails the build
rather than silently scribbling past the cell.

Orderings are spelled into the function names (`_relaxed`, `_acquire`,
`_release`) rather than passed as a parameter, because the ordering has to be
selected at compile time anyway and a named function is both clearer at the call
site and cheaper to read than a `comptime if` ladder. `fetch_add` is
sequentially consistent; that is what a work-queue counter wants.

## Why `Cell` comes from outside this file

`std.atomic.Atomic` changed its parameter *kind* under this tin once already:

| toolchain | declaration |
|---|---|
| Mojo 1.0.0 | `struct Atomic[dtype: DType, *, scope: StaticString = ""]` |
| Mojo 1.2.0, nightly | `struct Atomic[T: Deinitable & Movable, *, scope: ...]` |

so `Atomic[DType.int64]` was the only spelling 1.0.0 accepted and `Atomic[Int64]`
is the only spelling the two toolchains supported today accept. Everything else
was identical — the same `fetch_add[ordering=…]`, `load[ordering=…]`,
`store[ordering=…]` and `fence` calls compile unchanged against any of them.
The divergence was one type expression and nothing more.

Mojo offers no way to pick a *type* at module scope from a compiler-version
predicate: a `comptime if` around a `comptime Cell = …` does not parse on either
toolchain, and `-D` defines (`get_defined_bool` plus a `comptime if`) can only
branch inside a function body. So the choice is made by the include path
instead. `compat/threads_compat.mojo` declares `Cell` and nothing else, and a
build says so:

```sh
mojo build … -I src -I compat
```

Dropping Mojo 1.0.0 left that directory with one occupant rather than two: 1.2.0
and the 1.3.0.dev nightlies agree on every spelling here. The seam stays because the
next divergence is a matter of when, not whether, and re-splitting `compat/` into
one directory per toolchain is then a two-line change in this tin and a path edit
in each consumer.

This used to be solved one level *down* instead, by reaching past `std.atomic`
to the `pop.atomic.rmw` / `pop.load` / `pop.store` intrinsics it is itself
written on top of, on the reasoning that a compiler primitive would be steadier
than a stdlib signature. That held until it did not: nightlies from
`26.6.0.dev2026090105` onward reject the `__mlir_attr` syntax those calls need
(`error: invalid MLIR attribute: expected '<'`, filed as modular/modular#7094),
and because this was the only file in the whole stack touching raw MLIR it was
single-handedly capping every downstream tin below that nightly. Two spellings
of one alias, in a directory the caller selects, is a far smaller and more
legible surface than a private compiler ABI — and when it breaks it breaks in
Mojo the language rather than in an attribute grammar nobody documents.

That particular regression was fixed upstream the same day it was reported, so
the intrinsics would compile again today. This module is not going back to
them. The reasoning that sent it there — that a compiler primitive is steadier
than a stdlib signature — was wrong in the way that matters: `std.atomic` is
the interface with a compatibility promise, and `pop.*` is an implementation
detail of it. Two spellings of one alias is the cost of being on the supported
side of that line, and it is a cost worth paying whether or not the
undocumented side happens to work this week.

Expect `compat/` to grow again. Stable and nightly diverge as readily as they
converge, and `async` is the next thing this tin will want that they may well
spell differently. The pattern to follow is the one here: the divergent file
holds the declaration that differs and nothing whatsoever besides, so that a
reader can see the entire delta between the two toolchains at a glance and every
line that actually does work stays single-sourced.

## Structs

### `AtomicCounter`

```mojo
struct AtomicCounter
```

A shared 64-bit counter: a *view* over a cell somebody else owns.

The canonical use is a work queue. Every worker calls `fetch_add(1)` and
treats the returned value as its task index; the first worker to see an
index at or past the task count stops.

```mojo
while True:
    var i = Int(counter.fetch_add(1))
    if i >= n_tasks:
        break
    do_work(i)
```

Copying an `AtomicCounter` copies the view, never the cell — two copies
address the same counter, which is exactly what you want when you hand one
to each worker.

The cell must be 8-byte aligned and must outlive every view of it. `alloc`
satisfies the alignment; the lifetime is on you.

Implements: `Copyable`, `ImplicitlyCopyable`, `Movable`

#### `at`

```mojo
def at(address: Int) -> Self
```

View the counter cell at a raw address.

Args:

- `address`: Address of an 8-byte-aligned 64-bit cell.

```mojo
def at(ctx: Pointer[UInt8, MutUntrackedOrigin], slot: Int) -> Self
```

View the `slot`-th 64-bit cell of a context block.

Args:

- `ctx`: Base of a block of 64-bit cells, itself 8-byte aligned.
- `slot`: Which cell, counted in 8-byte units.

#### `alloc`

```mojo
def alloc(initial: Int64 = Int64(0)) -> Self
```

Allocate a fresh counter cell on the heap.

The view returned owns nothing — call `unsafe_free` on exactly one copy
once every thread that could touch it has been joined.

Args:

- `initial`: Starting value.

#### `unsafe_free`

```mojo
def unsafe_free(self)
```

Free a cell obtained from `alloc`. Never call this while a thread that can see the cell is still running.

#### `address`

```mojo
def address(self) -> Int
```

Return the raw address of the cell.

#### `fetch_add`

```mojo
def fetch_add(self, delta: Int64 = Int64(1)) -> Int64
```

Atomically add and return the previous value.

Args:

- `delta`: The amount to add.

#### `load`

```mojo
def load(self) -> Int64
```

Atomically read the counter (acquire).

#### `store`

```mojo
def store(self, value: Int64)
```

Atomically write the counter (release).

Args:

- `value`: The value to write.

### `AtomicFlag`

```mojo
struct AtomicFlag
```

A one-way (or resettable) publish/observe flag over a shared cell.

`set` is a *release* store and `is_set` is an *acquire* load, so the pair
carries data with it:

```mojo
# publisher
payload[0] = 42        # plain write
flag.set()             # release

# observer
while not flag.is_set():   # acquire
    spin_hint()
assert payload[0] == 42     # guaranteed visible
```

That release/acquire pairing is the whole point — a plain `Bool` written
from one thread and read from another has no such guarantee, and the
compiler is free to hoist the read out of the spin loop entirely.

Like `AtomicCounter`, this is a view: copying it does not copy the cell.

Implements: `Copyable`, `ImplicitlyCopyable`, `Movable`

#### `at`

```mojo
def at(address: Int) -> Self
```

View the flag cell at a raw address.

Args:

- `address`: Address of an 8-byte-aligned 64-bit cell.

```mojo
def at(ctx: Pointer[UInt8, MutUntrackedOrigin], slot: Int) -> Self
```

View the `slot`-th 64-bit cell of a context block.

Args:

- `ctx`: Base of a block of 64-bit cells, itself 8-byte aligned.
- `slot`: Which cell, counted in 8-byte units.

#### `alloc`

```mojo
def alloc() -> Self
```

Allocate a fresh, cleared flag cell on the heap.

#### `unsafe_free`

```mojo
def unsafe_free(self)
```

Free a cell obtained from `alloc`. Never call this while a thread that can see the cell is still running.

#### `address`

```mojo
def address(self) -> Int
```

Return the raw address of the cell.

#### `set`

```mojo
def set(self)
```

Publish: release-store 1. Everything written before this call is visible to a thread that later observes `is_set()`.

#### `clear`

```mojo
def clear(self)
```

Reset the flag to 0 with a release store.

#### `is_set`

```mojo
def is_set(self) -> Bool
```

Observe: acquire-load.

#### `raw`

```mojo
def raw(self) -> Int64
```

Return the cell's value as an integer (acquire load) — useful when a flag doubles as a small error code.

#### `set_value`

```mojo
def set_value(self, value: Int64)
```

Release-store an arbitrary non-zero value, for the flag-as-error-code pattern.

Args:

- `value`: The value to publish.

## Functions

### `atomic_fetch_add`

```mojo
def atomic_fetch_add(ptr: Pointer[Int64, MutUntrackedOrigin], delta: Int64) -> Int64
```

Atomically add `delta` to the cell, sequentially consistent.

Args:

- `ptr`: The cell.
- `delta`: The amount to add.

### `atomic_load_relaxed`

```mojo
def atomic_load_relaxed(ptr: Pointer[Int64, MutUntrackedOrigin]) -> Int64
```

Atomically read the cell with relaxed ordering.

Args:

- `ptr`: The cell.

### `atomic_load_acquire`

```mojo
def atomic_load_acquire(ptr: Pointer[Int64, MutUntrackedOrigin]) -> Int64
```

Atomically read the cell with acquire ordering.

Args:

- `ptr`: The cell.

### `atomic_store_relaxed`

```mojo
def atomic_store_relaxed(ptr: Pointer[Int64, MutUntrackedOrigin], value: Int64)
```

Atomically write the cell with relaxed ordering.

Args:

- `ptr`: The cell.
- `value`: The value to write.

### `atomic_store_release`

```mojo
def atomic_store_release(ptr: Pointer[Int64, MutUntrackedOrigin], value: Int64)
```

Atomically write the cell with release ordering.

Everything this thread wrote before the call becomes visible to any thread
that observes this write through `atomic_load_acquire`.

Args:

- `ptr`: The cell.
- `value`: The value to write.

### `atomic_fence_acquire`

```mojo
def atomic_fence_acquire()
```

A standalone acquire fence.

`std.atomic.fence` covers standalone fences on both toolchains with the same
`Ordering` the loads and stores take, so these two moved across with the
rest and no intrinsic is left in this file.

### `atomic_fence_release`

```mojo
def atomic_fence_release()
```

A standalone release fence. See `atomic_fence_acquire`.

## Aliases

### `CellPtr`

```mojo
comptime CellPtr = Pointer[Atomic[Int64], MutUntrackedOrigin]
```

A pointer to a shared atomic cell — what every primitive below actually operates on, obtained by bitcasting the caller's `I64Ptr`.
