Shared\Atomic
OxPHP\Shared\Atomic is a process-wide atomic 64-bit signed integer with the full primitive surface: load, store, swap, compareAndSet, plus fetchAdd/Sub/And/Or/Xor. Every operation is lock-free, and memory ordering is explicit and defaults to SeqCst.
Overview
- The value is an atomic int64 primitive with the range
−9_223_372_036_854_775_808 … 9_223_372_036_854_775_807. Overflow wraps. - Every operation is lock-free and compiles to a single CPU atomic instruction (
load,store,xchg,cmpxchg,xadd, etc.). - You pick the memory ordering. Pass an
OxPHP\Shared\Orderingenum value when you needRelaxed/Acquire/Release/AcqRel/SeqCst. Defaults areSeqCst, so callers who don't care get the strongest guarantee.
When to use Atomic instead of Shared\Counter:
- State machines, using
compareAndSetforidle → busy → done. - Version stamps and generation counters.
fetchAdd(1)returns the previous version, which readers can use to detect races. - CAS loops: read with
load, compute the new value, and retrycompareAndSetuntil it succeeds. - Bitflag masks, with
fetchOrto set bits andfetchAndto clear them.
Counter is the right tool for accumulation (add); Atomic is the right tool for arbitrary atomic state.
API Reference
namespace OxPHP\Shared;
final class Atomic implements Shareable
{
public function __construct(int $initial = 0);
public function load(Ordering $order = Ordering::SeqCst): int;
public function store(int $value, Ordering $order = Ordering::SeqCst): void;
public function swap(int $value, Ordering $order = Ordering::SeqCst): int; // returns prev
public function compareAndSet(
int $expect,
int $new,
Ordering $success = Ordering::SeqCst,
Ordering $failure = Ordering::SeqCst,
): bool;
public function fetchAdd(int $delta, Ordering $order = Ordering::SeqCst): int; // returns prev
public function fetchSub(int $delta, Ordering $order = Ordering::SeqCst): int; // returns prev
public function fetchAnd(int $mask, Ordering $order = Ordering::SeqCst): int; // returns prev
public function fetchOr (int $mask, Ordering $order = Ordering::SeqCst): int; // returns prev
public function fetchXor(int $mask, Ordering $order = Ordering::SeqCst): int; // returns prev
public function id(): int;
}| Method | Returns | Use case |
|---|---|---|
load |
current | Read the value with chosen ordering. |
store |
void | Write a new value, discarding the old. |
swap |
previous | Atomic replace; swap(0) is the snapshot-and-zero pattern. |
compareAndSet |
swapped? | Optimistic transitions and CAS loops. |
fetchAdd/Sub |
previous | Generation counters, bounded counters via CAS, deltas. |
fetchAnd/Or/Xor |
previous | Bitflag masks: set, clear, toggle. |
id |
registry id | Logging, tracing, /__ox_shared/entry?id=… correlation. |
Memory ordering
Short primer:
- Relaxed gives atomicity only, with no ordering relative to other memory accesses.
- Acquire (loads) pairs with a
Releasestore. Reads after this op observe writes the releaser had completed. - Release (stores) pairs with an
Acquireload, and writes before this op are visible to acquirers. - AcqRel (read-modify-write) combines both halves of an Acquire load and a Release store.
- SeqCst gives a single global total order across all
SeqCstoperations.
Each operation accepts only orderings that are meaningful for it:
| Operation | Allowed |
|---|---|
load |
Relaxed, Acquire, SeqCst |
store |
Relaxed, Release, SeqCst |
swap, fetchAdd, fetchSub, fetchAnd, fetchOr, fetchXor |
any |
compareAndSet success |
any |
compareAndSet failure |
Relaxed, Acquire, SeqCst |
Defaults are Ordering::SeqCst everywhere, so callers who don't think about ordering still get safe behaviour. An invalid combination throws OxPHP\Shared\InvalidOrderingException before the FFI call.
For the deep dive on the C++/Rust memory model, see the Rust std::sync::atomic::Ordering docs.
Examples
State machine via compareAndSet
<?php
use OxPHP\Shared\Atomic;
$state = new Atomic(initial: 0); // 0=idle, 1=busy, 2=done
if (!$state->compareAndSet(expect: 0, new: 1)) {
throw new RuntimeException('another worker is already processing');
}
try {
doWork();
$state->store(2);
} catch (Throwable $e) {
$state->store(0); // release back to idle on error
throw $e;
}Generation counter / version stamp
<?php
$version = new OxPHP\Shared\Atomic();
// Each writer bumps the version and gets the value it just superseded.
$prev = $version->fetchAdd(1);
publishUpdate($prev + 1, $payload);Optimistic update via CAS loop
<?php
use OxPHP\Shared\Atomic;
use OxPHP\Shared\Ordering;
$cell = new Atomic(initial: 100);
// Saturate-add: never go above 1000.
do {
$cur = $cell->load(Ordering::Acquire);
$next = min($cur + 7, 1000);
if ($cur === $next) {
break; // already at cap
}
} while (!$cell->compareAndSet($cur, $next, Ordering::AcqRel, Ordering::Acquire));Bitflag mask
<?php
const FLAG_READY = 1 << 0;
const FLAG_DRAINING = 1 << 1;
const FLAG_FAILED = 1 << 2;
$flags = new OxPHP\Shared\Atomic();
$flags->fetchOr(FLAG_READY); // set bit
$flags->fetchAnd(~FLAG_DRAINING); // clear bit
$snapshot = $flags->load();
if ($snapshot & FLAG_FAILED) {
raiseAlert();
}Semantics and gotchas
fetchAdd returns the previous value, not the new one. This deliberately contrasts with Counter::add, which returns the new total. The two are different abstractions with different return conventions, so pick the class that matches the semantics you mean.
i64::MIN.fetchSub(1) yields i64::MAX. No exception is raised.
SeqCst is the safest choice and the slowest. Drop to Acquire/Release/Relaxed only when you can articulate why.
An Atomic holds a single int64. For compound state (multiple coupled fields), use Shared\Mutex.
Exceptions
| Exception | Raised by |
|---|---|
StaleHandleException |
Any method on a handle whose registry entry was evicted. |
UninitializedException |
id() on a wrapper that has not finished __construct. |
InvalidOrderingException |
An operation receives a memory ordering invalid for it. |
Observability
See Shared Observability for the full tour. Quick references:
GET /__ox_shared/entry?id=Nexposes{ value, type: "Atomic" }.- Registry-wide counters (
oxphp_shared_operations_total,oxphp_shared_objects_total) cover Atomic via thetype="Atomic"label.
When not to use
- For compound state, where multiple fields must update together, use
Shared\Mutex. - Counting and accumulation belong in
Shared\Counter, whoseaddreturns the new total and so matches the domain. - Floats and decimals are not supported. Wrap a struct in
Shared\Mutex, or pair two Counters (numerator / denominator). - Atomic is in-process only, so it cannot coordinate across hosts. For multi-host state use Redis, a database, or a metric pipeline.
- Atomic state is not durable; it evaporates at server stop. Persist snapshots elsewhere if the value must survive restarts.
Prompts for your agent
These prompts assume an agent with the OxPHP documentation MCP server connected. The agent reads the pages it needs itself.
Use an OxPHP Shared Atomic in my code instead of a status flag or bitmask I update with a plain read-then-write. Read the `shared-state/shared-atomic` page through the OxPHP documentation MCP server.
Find one of those in this repository and show me the `compareAndSet`-based replacement, including which memory ordering it actually needs.Related
- Shared State: overview and migration patterns.
- Shared\Counter: when the value is a domain accumulator.
- Shared\Mutex: when state spans more than one int64.
- Shared\Flag: when the value is just on/off.