// 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 pts

Implement 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 n slots held at once.
  • TryAcquire never blocks: it returns false when full.
  • Release makes the slot available again; calling Release when 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.