Files
learning-go-for-sre-interview/09-safe-counter/README.md
T
kacerrandClaude Fable 5 7219eb5a23 Initial commit: Go SRE interview exercises
Includes solved 02-worker-pool and 09-safe-counter (mutex, atomic, sharded).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GbnJj9ZqEx9nUvzkjdwfMq
2026-08-28 22:18:09 +02:00

55 lines
2.9 KiB
Markdown

# 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.