Atomics & the Memory Model
sync/atomic looks like the cheapest way to synchronise — a single indivisible operation instead of Lock/Unlock. And indeed, guarding a counter, a flag, or a pointer with an atomic is faster and simpler than with a mutex. But the whole Go memory model hides behind that simplicity, and that is what a serious interview checks — not the syntax of atomic.AddInt64.
There are two sharp edges here. The first — an atomic is indivisible only for its own variable: two atomics cannot guard an invariant that ties two values together, and mixed atomic-and-plain access to one variable stays a data race. The second, deeper one — the memory model: without an explicit happens-before edge, one goroutine is not obliged to see another's writes and may even see them in a different order. A data race in Go is not "an occasional stale read" but undefined behaviour. This topic dissects atomics layer by layer — from the five operations of the sync/atomic package to how the LOCK prefix and a memory fence turn a CPU instruction into a synchronization edge.
Topic Map
- The sync/atomic package — lock-free operations on a single machine word:
Load,Store,Add,Swap,CompareAndSwap, the typedatomic.Int64/atomic.Valuewrappers, and the CAS loop. - CPU-level atomics — the
LOCKprefix on x86, the load-linked/store-conditional pair on ARM, memory fences, and why a contended atomic scales poorly. - The Go memory model — happens-before rules, the synchronization edges of channels, mutexes, and
sync.Once, and a data race as undefined behaviour. - Atomics vs mutex — when one indivisible operation on a single word is enough, and when you need a full critical section under a mutex.
Common Mistakes and Traps
| Mistake | Consequence |
|---|---|
| Mixing atomic and plain access to the same variable | The data race remains; undefined behaviour — only uniformly atomic access is safe |
| Guarding a multi-step invariant or a pair of values with atomics | An atomic covers only one word; the intermediate state is visible to other goroutines |
| Assuming an atomic store also publishes neighbouring variables | An atomic orders only itself, not adjacent writes |
Believing an aligned MOV is atomic across cores by itself | Without the LOCK prefix and a fence, other cores will not see the result consistently |
| Thinking an atomic makes a system call or locks a global mutex | It is a CPU instruction with no trip into the kernel; only one cache line is touched |
| Assuming an atomic is always faster than a mutex | Under high contention a CAS loop spins uselessly and loses to a mutex — benchmark it |
| Assuming writes are visible across goroutines without a synchronization edge | Stale reads or a race — the memory model gives no ordering without happens-before |
| Reversing the edge direction — "receive happens-before send" | A wrong conclusion about the scheduling order of reader and writer |
| Relying on CAS without a retry loop | CAS may fail if the value changed meanwhile; you need a read–compute–write loop |
Interview Relevance
Atomics and the memory model are what separate a candidate who can reason about the correctness of concurrent code from someone who memorised go func(){}(). Channels and the mutex are usually known; here the interviewer checks whether you have a working mental model of write visibility between goroutines.
What interviewers check:
- What
sync/atomicprovides and why an atomic guards only a single machine word. - Why mixed atomic-and-plain access to a variable stays a data race.
- How an atomic operation is implemented at the CPU level — the
LOCKprefix, LL/SC, memory fences. - Why a contended atomic scales poorly and when a mutex is faster.
- What happens-before is and which synchronization edges establish it.
- What a data race is in Go and why it is undefined behaviour, not just a stale read.
A typical wrong answer: "atomics are just a fast mutex, you can guard anything with them." That triggers a discussion that an atomic indivisibly updates exactly one word, while consistency across several values or a multi-step critical section needs a sync.Mutex.