Atomic operation

On this page 7

An atomic operation is an operation on shared data that executes as a single, indivisible step from the point of view of every other thread: no thread can observe it half finished, and no other operation on the same location can interleave with it. In concurrency, atomic operations are the lowest-level building blocks for correct communication between threads.

Hardware provides atomic instructions such as atomic load and store, fetch-and-add, exchange and compare-and-swap (CAS), and programming languages expose them through types such as std::atomic in C++, AtomicInteger in Java and Interlocked methods in .NET. Mutexes, semaphores and lock-free data structures are all built on top of them.

How atomic operations prevent interleaving

A seemingly simple statement such as counter++ is actually three separate steps: read the value, add one, write the result. If two threads perform these steps at the same time, both can read the same old value and one increment is lost. This is a classic race condition, and an atomic increment eliminates it by making the read-modify-write sequence indivisible.

On modern processors, atomicity is enforced by the cache coherence protocol. When a core executes an atomic read-modify-write instruction, it obtains exclusive ownership of the cache line holding the variable, so no other core can modify that line until the instruction completes. On x86 this is expressed with the LOCK prefix; on ARM and RISC-V it is implemented with load-linked and store-conditional pairs or dedicated atomic instructions.

The table below lists the most common atomic primitives and what each guarantees.

PrimitiveWhat it doesTypical use
Atomic load and storeReads or writes a value without tearingFlags, published pointers
Fetch-and-addAdds a value and returns the old oneCounters, ticket numbers
ExchangeWrites a value and returns the old oneSimple spinlocks
Compare-and-swapWrites only if the current value matches an expected oneLock-free structures
Test-and-setSets a bit and returns its previous stateMinimal locks
Common atomic primitives

Compare-and-swap and lock-free algorithms

Compare-and-swap is the most powerful common primitive because it allows arbitrary updates. A thread reads the current value, computes a new one and asks the processor to store it only if the value has not changed in the meantime. If another thread modified it, the CAS fails and the thread retries with the fresh value, a pattern known as a CAS loop.

CAS loops are the basis of lock-free data structures such as stacks, queues and reference counters, in which at least one thread always makes progress without blocking. They come with a classic pitfall, the ABA problem: a value changes from A to B and back to A, and a CAS wrongly concludes that nothing happened. Version counters, tagged pointers and safe memory reclamation schemes are the usual defenses.

Memory ordering and visibility

Atomicity alone guarantees that a single operation is indivisible; it does not automatically order surrounding reads and writes. Compilers and processors reorder memory accesses for speed, so languages such as C++ and Rust attach a memory ordering to each atomic operation to control what other threads are guaranteed to see.

OrderingGuaranteeRelative costTypical use
RelaxedAtomicity only, no orderingLowestStatistics counters
AcquireLater accesses stay after the loadLowReading a ready flag
ReleaseEarlier accesses stay before the storeLowPublishing data
Acquire-releaseBoth, for read-modify-writeModerateLock implementations
Sequentially consistentSingle global order of all such operationsHighestDefault, simplest reasoning
Memory orderings in the C++ memory model

The pairing of a release store with an acquire load of the same variable creates a happens-before relationship: everything written before the release becomes visible to the thread that observes it through the acquire. Sequential consistency is the safe default; weaker orderings should be used only when profiling shows a need and the reasoning is carefully reviewed.

Atomic operations compared with locks and other tools

Atomic operations and locks solve related problems at different granularity. An atomic operation protects one memory location for one operation, while a mutex protects an arbitrary critical section that may touch many variables.

NeedSuitable toolReason
Increment a shared counterAtomic fetch-and-addOne location, no lock overhead
Set a stop flagAtomic booleanSimple publication of a state
Update several related fieldsMutexAtomics cannot combine locations
Limit concurrent access to NSemaphoreCounts available permits
High-throughput queueLock-free structure using CASAvoids blocking under contention
Choosing between atomic operations and other synchronization

In the actor model or with message passing, shared mutable state is avoided altogether, so application code rarely uses atomics directly, although the runtime relies on them internally to implement mailboxes and schedulers.

Pitfalls and performance considerations

The most common error is assuming that two atomic operations together are atomic. Checking an atomic value and then updating it in a separate step reintroduces a race, which is exactly what compare-and-swap exists to prevent.

  • Contention cost: many cores updating one atomic variable bounce its cache line between them, which can be slower than expected.
  • False sharing: unrelated atomics on the same cache line slow each other down; padding or alignment separates them.
  • Volatile is not atomic: in C and C++, volatile prevents some compiler optimizations but provides neither atomicity nor ordering between threads.
  • Wide types: values larger than the native word may not be lock-free on every platform, so implementations may fall back to internal locks.
  • Overly weak ordering: relaxed operations used for publication can expose partially initialized data.

For heavily contended counters, sharded counters that give each thread or core its own slot, combined at read time, often scale far better than a single atomic variable.

The simple explanation

An atomic operation is an action that happens all at once or not at all. Nobody can catch it halfway done.

Imagine two cashiers updating the same paper tally of sold tickets. Each reads the number, adds one and writes it down. If both read 50 at the same moment, both write 51, and one sale disappears. An atomic operation is like a counter with a single button that adds one per press, so no press is ever lost. The key takeaway is that atomic operations keep shared numbers correct when many workers change them simultaneously.

How it is used in practice

A web server handles requests on 32 worker threads and keeps a running count of served requests for its monitoring endpoint. The first version uses an ordinary integer, and under load the reported total is consistently lower than the count in the access logs, because concurrent increments overwrite each other.

The developers replace the integer with an atomic counter using fetch-and-add with relaxed ordering, since only the total matters. The counts now match exactly. Later, profiling shows the shared counter’s cache line as a hot spot, so they switch to per-thread counters summed when the endpoint is read. The result is accurate statistics with no measurable effect on request throughput.