Haiku Atomic Operations: Value Updates, Ordering, and Publication
Use Haiku atomic operations with the correct return-value and acquire/release semantics, while avoiding compound-state races and false lock-free guarantees.
Haiku exposes integer atomic operations through SupportDefs.h, including atomic_get, atomic_set, atomic_add, atomic_and, atomic_or, compare-and-set helpers, and 64-bit variants. They are useful for a small shared value whose updates must not be torn or lost. They are not a general synchronization design, a replacement for a mutex around a multi-field invariant, or proof that an operation is lock-free on every supported architecture.
The exact memory ordering matters. In the current Haiku header’s compiler-builtin implementation, atomic_set() uses a release store and atomic_get() uses an acquire load. The fetch-add, exchange, bitwise, and compare-exchange operations use sequentially consistent orderings in that implementation. Older compiler paths are declared as external functions, so code should rely on Haiku’s documented API contract for the target release rather than assuming how a different compiler lowers the operation.
Start with the invariant, not the primitive
Before choosing an atomic function, write down the state transition that must remain consistent. A single counter increment is a good candidate for atomic_add(). A flag that publishes fully initialized data can use a release/acquire pair. A set of fields that must change together usually needs a lock, an immutable snapshot, or a carefully designed atomic state word.
An atomic operation protects its target value from data races at that operation. It does not make unrelated fields atomic, reserve a resource, or ensure that a later callback observes an entire logically consistent object. If one thread updates a pointer and another updates a length, separately making both fields atomic can still permit a reader to observe the new pointer with the old length.
The API takes pointers to int32 or int64. The storage must be correctly aligned and must remain alive for every concurrent access. Do not cast an unrelated packed field or a pointer to a different integer width to satisfy the signature. Avoid mixing atomic operations with ordinary reads and writes to the same shared location; use the atomic API consistently or guard all access with the same lock.
Use acquire and release for publication
Release and acquire form a one-way publication relationship when the acquire observes the released value. Initialize the payload first, then publish a flag with atomic_set(). A reader checks the flag with atomic_get() before consuming the payload. On a target using the compiler-builtin implementation described above, this makes the initialization visible to a reader whose acquire load observes that release store.
#include <SupportDefs.h>
#include <string.h>
struct SharedSnapshot {
int32 value;
char label[64];
};
static SharedSnapshot sSnapshot;
static int32 sReady = 0;
void PublishSnapshot(int32 value, const char* label)
{
if (label == nullptr)
return;
sSnapshot.value = value;
strlcpy(sSnapshot.label, label, sizeof(sSnapshot.label));
atomic_set(&sReady, 1); // Release publication after payload writes.
}
bool TryReadSnapshot(SharedSnapshot* output)
{
if (output == nullptr || atomic_get(&sReady) == 0)
return false; // Acquire before reading the published payload.
*output = sSnapshot;
return true;
}
This sketch assumes one publisher and has a deliberate single-publication limitation: after sReady becomes nonzero, no writer may rewrite sSnapshot concurrently with readers. The flag makes prior initialization visible; it does not protect a mutable payload from later races. For repeated updates, use a lock or a proven sequence-counter pattern with the required atomic fields and memory ordering. Do not copy this one-shot example into a continuously updated telemetry object.
An acquire load that reads an unrelated value does not magically synchronize with every writer. Define who writes the flag, whether it can be reset, how many writers exist, and whether readers can run before publication. If the state cycles from ready to not-ready to ready, include a generation or use a synchronization mechanism that prevents ABA-style confusion.
Understand return values and compare-and-set
Haiku’s atomic arithmetic and bitwise functions return the previous value according to the public header comments. That detail matters for counters and loops: atomic_add(&counter, 1) returns the old count, not necessarily the new count. If code needs the updated value, derive it carefully or perform a subsequent atomic read, understanding that another thread may already have changed it again.
atomic_test_and_set() compares the value to a caller-specified test value and conditionally writes a new value. Its return convention is less obvious than a C++ compare_exchange boolean, so check the installed header before using it. There is an additional source-level caveat in the current compiler-builtin path: it calls __atomic_compare_exchange_n() with the weak flag set. A weak compare-exchange may fail spuriously while leaving the expected value unchanged, so a helper that interprets “returned expected value” as a successful claim can misreport success. Do not build a lock around this helper without resolving that distinction for the actual target implementation.
For a simple counter, use the operation whose previous-value behavior is explicit in the header:
#include <SupportDefs.h>
static int32 pendingWork = 0;
void WakeWorker(); // Supplied by the queue owner.
void
NoteWorkQueued()
{
int32 previous = atomic_add(&pendingWork, 1);
if (previous == 0)
WakeWorker();
}
This is appropriate only if the protocol means that the transition from zero to one should wake the worker and all accesses to pendingWork use the atomic API. It does not make the work queue itself thread-safe. If the protected region needs blocking, ownership, fairness, or cleanup, use a semaphore or lock instead of spinning.
Do not infer lock-freedom or pointer safety
The existence of a C function does not promise that every architecture implements it as one hardware instruction. Some operations can compile to library calls or synchronization code. The header’s older-compiler fallback is an explicit reminder that implementation strategy can vary. If latency or lock-freedom is a hard requirement, measure and inspect the target toolchain and architecture; do not claim it from the function name.
The exposed API is integer-focused. Do not cast a pointer to int32 or int64 and call an integer atomic function; pointer width, alignment, and representation can differ. If you need atomic pointer publication, use a toolchain-supported pointer atomic with a documented Haiku-compatible contract or synchronize with a lock.
Atomic counters also need overflow policy. Signed integer overflow in C++ is not a safe wraparound contract. Bound the value, use a wider type when it matches the API, or define saturation behavior through a compare-and-set loop. A reference count should not be decremented below zero or resurrected after destruction; atomic arithmetic alone does not solve object lifetime.
Combine atomics with ownership and shutdown rules
Use atomics to coordinate a small state bit, but decide how objects remain alive. A flag saying “worker stopped” cannot make it safe to destroy data while a worker may still be reading it. Join the thread or establish a synchronization boundary before freeing shared memory. Similarly, an atomic increment on a pointer’s counter does not make acquiring a reference safe if another thread can destroy the object first.
Avoid using a global atomic counter as an implicit event queue. Counters tell you a quantity, not which work item arrived or in what order. If multiple producers must publish distinct jobs, use a port, message queue, or a proper concurrent queue. If a UI control receives updates from worker threads, send a message to its looper rather than relying on an atomic value to make cross-thread view mutation safe.
Test memory ordering and failure paths
Test with multiple threads under a race detector where available, but remember that a clean run does not prove a memory-order design correct. Review the happens-before relationship explicitly. Test initialization, repeated publication, cancellation, thread shutdown, overflow boundaries, compare-and-set contention, and a reader racing with teardown. Add a stress test that checks invariants, not only that the process did not crash.
If using 64-bit operations on a 32-bit build, confirm the target supports the API and measure the actual implementation. Keep the simplest lock-based design unless atomic operations materially improve a measured bottleneck. A correct mutex is usually easier to review than an improvised spin loop.
Haiku atomics are small tools for shared integer state. Use the documented ordering, treat return values exactly, avoid mixing atomic and ordinary access, and keep multi-field invariants and object lifetime under a higher-level synchronization design.
Related:
- Haiku BLocker and BAutolock: Recursive Locks, Timeouts, and Lifetime
- Haiku Kernel Semaphores: Counting, Timeouts, and Safe Shutdown
Sources: