Atomic Counters
The last lesson's SafeCounter guarded a map[string]int with a
sync.Mutex, and that was the right tool: a map has no atomicity
guarantees of its own, and a mutex is how you make "read the map, decide,
write the map" happen as one uninterruptible step. Now shrink the problem —
one int64, incremented from many goroutines, nothing else — and a mutex
is more machinery than the job needs: allocate a lock, Lock, increment,
Unlock, on every single call, when the CPU already has an instruction that
does "increment this memory location, indivisibly" in one step.
sync/atomic
type Counter struct {
n atomic.Int64
}
func (c *Counter) Inc() {
c.n.Add(1)
}
func (c *Counter) Value() int64 {
return c.n.Load()
}
atomic.Int64 (and its siblings — Int32, Uint64, Bool, Pointer[T],
added in Go 1.19) wraps a single value with methods — Add, Load,
Store, CompareAndSwap — that the hardware executes as one indivisible
operation. No Lock/Unlock pair, no goroutine ever blocks waiting for
another to finish; the CPU guarantees two concurrent Add calls never
interleave into a lost increment, the same bug the mutex lesson opened with,
solved a level lower.
The older API — atomic.AddInt64(&n, 1) on a plain int64 — still exists
and still works, but it's easy to misuse: nothing stops you from also doing
n++ somewhere else in the code, silently reintroducing the race, because a
plain int64 doesn't know it's supposed to be accessed atomically. The
typed wrappers make that mistake harder — Counter.n is an atomic.Int64,
not an int64, so there's no bare field to accidentally touch un-atomically
in a moment of forgetting.
Where atomic stops being the answer
Atomic operations cover exactly one word of memory at a time. The instant
you need two related values to change together — a count and a
timestamp, a balance and a transaction log — atomic can no longer help,
because "update these two things as one step" isn't a single CPU
instruction. That's not a limitation to work around; it's the actual
boundary between the two tools. One protected value, changed independently
of everything else: atomic. Several values that must move together, or
any access pattern more elaborate than increment/read: back to sync.Mutex
from the last lesson. Reaching for the wrong one in either direction is how
you end up with either needless lock contention or a mutex-free struct
that's quietly racy the moment a second field gets added.
Try it
Implement Counter using atomic.Int64 — Inc and Value, safe to call
from any number of goroutines at once, with no explicit locking.
Practice
1. An Atomic Counter
IntermediateImplement
Counterwith two methods:Inc()— increments the counter by 1.Value() int64— returns the current count.
Both must be safe to call concurrently from any number of goroutines, using
sync/atomic— nosync.Mutex.package main type Counter struct { // TODO: add a field } // Inc increments the counter. Safe for concurrent use. func (c *Counter) Inc() { // TODO: implement } // Value returns the current count. Safe for concurrent use. func (c *Counter) Value() int64 { // TODO: implement return 0 } func main() {}Sign in to write and run your solution in the browser.
Sign in for hints and the solution.