Atomics & the Memory Model
The sync/atomic package, CPU-level atomics, and the Go memory model with happens-before.
6 questions
JuniorTheoryVery commonWhat does the sync/atomic package provide?
What does the sync/atomic package provide?
sync/atomic provides lock-free operations on a single word — Load, Store, Add, Swap, and CompareAndSwap — that complete indivisibly. They prevent data races on simple counters and flags without a mutex, and the typed atomic.Int64/atomic.Value wrappers expose them safely.
Common mistakes
- ✗Mixing atomic and plain access to the same variable — only consistent atomic access is race-free
- ✗Believing atomics can protect a multi-step invariant — they only cover one word
- ✗Forgetting that a non-atomic 64-bit field on 32-bit platforms must be 8-byte aligned
Follow-up questions
- →Why must you never mix atomic and non-atomic access to the same variable?
- →What does CompareAndSwap return and how is it typically used in a loop?
MiddleTheoryCommonWhen should you use atomics instead of a mutex?
When should you use atomics instead of a mutex?
Use atomics when the shared state is a single word — a counter or a flag — updated by one indivisible operation. Use a sync.Mutex when you must keep several values consistent or run a multi-step critical section. Atomics are faster but only protect one variable.
Common mistakes
- ✗Using atomics to update several fields that must stay mutually consistent
- ✗Picking a mutex for a hot single-word counter where an atomic Add is enough
- ✗Believing an atomic store publishes unrelated variables — it orders only itself
Follow-up questions
- →How do you protect an invariant spanning two counters that must change together?
- →Why can a lock-free atomic loop still be slower than a mutex under high contention?
MiddleDebuggingCommonWhy does this count++ across 1000 goroutines not reliably reach 1000, and how do you fix it?
Why does this count++ across 1000 goroutines not reliably reach 1000, and how do you fix it?
count++ is a read-modify-write, not atomic. Many goroutines read the same value, increment, and write back, so increments are lost and the total is nondeterministic — go run -race flags the data race. Fix with a mutex around count++, or atomic.AddInt64(&count, 1) with count as int64.
Common mistakes
- ✗Believing
count++is a single atomic instruction rather than read-modify-write - ✗Blaming the
WaitGroupinstead of the unsynchronized shared write - ✗Thinking a
time.Sleepor cache flush fixes a data race
Follow-up questions
- →Why is
atomic.AddInt64cheaper than a mutex for a single counter? - →What does the
-racedetector actually observe to flag this?
SeniorTheoryOccasionalWhen can atomic operations be slower than a mutex?
When can atomic operations be slower than a mutex?
Atomics usually beat a mutex on a single word, but under high contention from many cores hammering one variable each atomic read-modify-write keeps invalidating that cache line across cores, so it bounces between CPUs (MESI coherency traffic) and throughput collapses — a mutex that parks waiters can win. Atomics also lose when several variables must stay consistent, since that needs many ops versus one critical section.
Common mistakes
- ✗Believing atomics are always faster, ignoring cache-line bouncing under contention
- ✗Using many atomic ops to coordinate several variables instead of one critical section
- ✗Assuming a lock-free atomic loop scales linearly when many cores hit one variable
Follow-up questions
- →Why does cache-coherency (MESI) traffic make a contended atomic loop slow down?
- →Why can a mutex that parks waiters out-throughput atomics under heavy contention?
SeniorTheoryRareHow are atomic operations implemented at the CPU instruction level?
How are atomic operations implemented at the CPU instruction level?
On x86 atomics compile to LOCK-prefixed instructions (LOCK XADD, LOCK CMPXCHG) that hold the cache line exclusively for the access. ARM uses an LL/SC pair (load-linked / store-conditional) that retries on conflict. Memory fences enforce ordering so other cores observe the result.
Common mistakes
- ✗Thinking an aligned MOV is atomic across cores without any LOCK prefix or fence
- ✗Believing atomics disable interrupts or trap into the kernel rather than locking a cache line
- ✗Assuming CAS always succeeds — LL/SC and CMPXCHG can fail and need a retry loop
Follow-up questions
- →Why does a contended atomic add scale poorly across many cores?
- →What is the ABA problem and how does it affect a CompareAndSwap loop?
SeniorTheoryRareWhat does the Go memory model guarantee about happens-before ordering?
What does the Go memory model guarantee about happens-before ordering?
The Go memory model defines happens-before: if event A happens-before B, A's writes are visible to B. A channel send happens-before the matching receive; a Mutex Unlock happens-before the next Lock. Without such a synchronisation edge, goroutines have no ordering guarantee at all.
Common mistakes
- ✗Assuming writes are visible across goroutines without an explicit synchronisation edge
- ✗Believing the source order of statements is preserved as seen by another goroutine
- ✗Reversing the direction — a send happens-before its receive, not the other way around
Follow-up questions
- →What happens-before edge does closing a channel establish for its receivers?
- →Why is a data race undefined behaviour in Go rather than just a stale read?