Data-Race Hunt (Go Memory Model)
Source: Custom
Topics: Go memory model, -race detector, sync.Mutex, sync/atomic
Problem
This package is the correctness sibling of pprof-bottleneck. counter.go and registry.go
compile and pass a single-threaded test — but both contain data races that the Go race
detector will flag under concurrent access.
Two types, two classic races:
Counter — concurrent Inc() + Value(). A c.n++ is read-modify-write: three steps
that interleave and lose updates.
Registry — concurrent Set() / Get() on a plain map. Maps are not goroutine-safe;
concurrent access can corrupt internal state and crash the runtime.
Task:
- Run
go test -race and watch it report the races (the concurrent tests are there but the
slow path will fail under -race).
- Fix
Counter two ways and keep whichever you prefer in the solution: with a sync.Mutex,
and with sync/atomic. Note which is faster in the benchmark and why.
- Fix
Registry with a sync.RWMutex (or sync.Map) — and think about why RWMutex helps a
read-heavy registry.
- All tests, including
-race, must pass.
Don't peek at solution/ until go test -race is green on your own version.
type Counter struct { ... }
func (c *Counter) Inc()
func (c *Counter) Value() int64
type Registry struct { ... }
func NewRegistry() *Registry
func (r *Registry) Set(key, val string)
func (r *Registry) Get(key string) (string, bool)
Detecting & reasoning
# The race detector instruments memory access and reports conflicting unsynchronized access.
go test -race ./challenges/profiling/data-race/
# Benchmark mutex vs atomic for the counter:
go test -bench=. ./challenges/profiling/data-race/
Key concepts
- Go memory model: without a happens-before relationship (established by a mutex, channel,
or
sync/atomic), one goroutine's writes are not guaranteed to be visible to another — and
the compiler/CPU may reorder them. A data race is undefined behavior, not "just a stale read".
c.n++ is not atomic: it's load, add, store. Two goroutines can both load the same value and
both store +1, losing an increment. atomic.Int64.Add(1) makes it a single indivisible op.
- Maps and concurrency: the runtime actively detects concurrent map read/write and panics
(
fatal error: concurrent map ...) — it's not protected by -race only, it can crash in prod.
- Mutex vs atomic: atomics are faster for a single word (no lock, no scheduler involvement) but
don't compose — once you guard multiple fields together, you need a mutex.
Run
go test -v -race -bench=. ./challenges/profiling/data-race/
Sign in to submit your solution.