// lesson: atomics-memory-model
Atomics and the Memory Model
The Go memory model answers one question: when is a read guaranteed to
observe a particular write? The answer is phrased as happens-before โ
a partial order built from program order within a goroutine plus
synchronization edges between goroutines. Channel operations, mutex
lock/unlock, WaitGroup.Wait, Once.Do, and sync/atomic operations
all create those edges. A write in one goroutine and a read in another
with no edge between them is a data race, and a racy Go program has no
defined behavior at all: the compiler and CPU are free to reorder, cache
in registers, and tear multi-word values. "It's just a flag, worst case
I read a stale bool" is not a claim the language honors.
sync/atomic gives you synchronization at the granularity of a single
word. Since Go 1.19 the memory model guarantees atomics behave
sequentially consistent โ an atomic write release-publishes everything
that happened before it, and an atomic read that observes it acquires
all of that history. That is why an atomic.Bool "initialized" flag
works: the flag write is the edge over which the initialized data
travels.
The composable primitive is compare-and-swap:
for {
old := counter.Load()
if old >= limit {
return false // full โ no state change needed
}
if counter.CompareAndSwap(old, old+1) {
return true // we won the race for this transition
}
// somebody else moved the state; re-read and retry
}
A CAS loop reads the current state, computes the successor state, and commits only if nothing changed in between โ lock-free optimistic concurrency in five lines. Losing a CAS is not failure, it just means another goroutine made progress; you retry against the fresh value.
Know the boundary: atomics suffice when the entire invariant fits in one word (a counter with a cap, a state enum, a flag). The moment an invariant spans two fields, two atomic operations are two separate edges with a hole between them โ that is mutex territory. And beware ABA: CAS only proves the value is the same, not that nothing happened.
โบ Lock-Free Slot Limiter
30 ptsImplement a lock-free bounded slot allocator โ the core of a
non-blocking semaphore โ using only sync/atomic (no mutexes, no
channels):
type Slots struct { /* ... */ }
func NewSlots(n int) *Slots // capacity n
func (s *Slots) TryAcquire() bool // claim a slot; false if all in use
func (s *Slots) Release() // return a slot; panic if none held
Invariants the tests enforce behaviorally, under heavy contention:
- Never more than
nslots held at once. TryAcquirenever blocks: it returnsfalsewhen full.Releasemakes the slot available again; callingReleasewhen no slot is held must panic (a limiter that silently grows its capacity is corrupted).
Use a CompareAndSwap loop for both transitions. (The tests exercise
behavior under contention; they cannot prove you avoided a mutex.
Lock-freedom is the exercise here, not something the grader asserts.)
Log in to submit a solution and earn points.