# 09 — Safe Counters, Three Ways Trivial-looking, but it's the interviewer's synchronization litmus test — and the benchmark at the end teaches something real about multicore hardware. ## What a data race actually is (get the definition razor-sharp) Two accesses to the same memory location, at least one a write, not ordered by a happens-before edge. Not "they happen at the same time" — *unordered*. The Go memory model promises basically nothing for racy programs (a racy read can observe a torn/stale/impossible value), so "it works on my machine" is not evidence of anything. `-race` instruments happens-before via vector clocks (ThreadSanitizer); it has ~5-10x slowdown, only catches races that *occur* during the run, and is still the best deal in software testing. Run integration environments with it if you can afford to. Bonus distinction interviewers fish for: **data race** (memory-level, defined above) vs **race condition** (logic-level, e.g. check-then-act on a file that another process swaps). You can have either without the other. ## Mutex vs atomic — when each - `atomic.Int64` for independent counters/flags/gauges: lock-free, a single LOCK-prefixed instruction on amd64. Use the typed API (`atomic.Int64`), not the legacy `atomic.AddInt64(&x, 1)` — the typed one can't be misused with an unaligned or copied address. - The moment an operation touches **two things that must stay consistent** (map + list in the LRU, tokens + timestamp in the limiter), atomics stop composing and you want a mutex. "Atomics don't compose" is the sentence. - `sync.Map`: only for append-mostly maps or disjoint per-goroutine keysets (its docstring says so). A mutexed map beats it for general workloads. ## Why sharding works: false sharing and cache lines Run the benchmarks with `-cpu=1,4,8`. The single-mutex counter gets *slower* with more cores: every Inc bounces the mutex's cache line between cores (cache-coherence traffic, MESI protocol), and cores stall waiting for exclusive ownership. Sharding gives each hot key set its own mutex on its own cache line → contention divides by shard count. The deep cut: even *separate* atomics hurt if they share a 64-byte cache line — **false sharing**. High-performance Go code pads hot fields (`_ [64]byte`) or uses `runtime/internal`-style per-P structures. This is exactly why Prometheus client_golang counters are fast: per-goroutine-ish sharded values summed on scrape. ## Snapshot-as-copy: the API design point Returning the internal map hands the caller a live reference into your critical section — they'll range over it while you write, and the runtime throws `concurrent map iteration and map write` (fatal, unrecoverable, not a panic you can catch). Copy under the lock, return the copy. General principle: **never let references escape the mutex**. Same reason you don't return internal slices from locked structs without copying.