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
132 lines
3.0 KiB
Go
132 lines
3.0 KiB
Go
// Exercise 09 — Concurrency-Safe Counters (three ways)
|
|
//
|
|
// ASSIGNMENT
|
|
// ----------
|
|
// A warm-up screener that interviewers run with -race. Implement THREE
|
|
// versions of a keyed counter and one plain counter:
|
|
//
|
|
// 1. MutexCounter: map[string]int64 guarded by a sync.Mutex.
|
|
// Inc(key), Get(key), Snapshot() (a COPY of the map — why a copy? if you
|
|
// return the internal map, the caller can race on it).
|
|
// 2. AtomicCounter: a single int64 total using sync/atomic only.
|
|
// Inc(), Get(). No mutex allowed.
|
|
// 3. ShardedCounter: N shards, each its own mutex+map, key routed by hash.
|
|
// Same interface as MutexCounter. This is how you fix mutex contention
|
|
// on hot maps.
|
|
//
|
|
// Run the tests: go test -race ./09-safe-counter/
|
|
// Then benchmark: go test -bench=. -cpu=1,4,8 ./09-safe-counter/
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"hash/fnv"
|
|
"sync"
|
|
"sync/atomic"
|
|
)
|
|
|
|
// ---- 1. Mutex version ----
|
|
|
|
type MutexCounter struct {
|
|
mu sync.Mutex
|
|
m map[string]int64
|
|
}
|
|
|
|
func NewMutexCounter() *MutexCounter {
|
|
return &MutexCounter{
|
|
m: make(map[string]int64),
|
|
}
|
|
}
|
|
func (c *MutexCounter) Inc(key string) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.m[key]++
|
|
|
|
}
|
|
func (c *MutexCounter) Get(key string) int64 {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
return (c.m[key])
|
|
}
|
|
func (c *MutexCounter) Snapshot() map[string]int64 {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
// possibly could use maps.clone
|
|
outmap := make(map[string]int64, len(c.m))
|
|
for k, v := range c.m {
|
|
outmap[k] = v
|
|
}
|
|
return outmap
|
|
}
|
|
|
|
// ---- 2. Atomic version ----
|
|
|
|
type AtomicCounter struct {
|
|
n atomic.Int64
|
|
}
|
|
|
|
func NewAtomicCounter() *AtomicCounter { return &AtomicCounter{} }
|
|
func (c *AtomicCounter) Inc() { c.n.Add(1) }
|
|
func (c *AtomicCounter) Get() int64 { return c.n.Load() }
|
|
|
|
// ---- 3. Sharded version ----
|
|
|
|
const shards = 16
|
|
|
|
type shard struct {
|
|
mu sync.Mutex
|
|
m map[string]int64
|
|
}
|
|
type ShardedCounter struct {
|
|
s [shards]shard
|
|
// route with fnv-1a: hash(key) % shards (hash/fnv or hand-rolled)
|
|
}
|
|
|
|
func NewShardedCounter() *ShardedCounter {
|
|
c := &ShardedCounter{}
|
|
// loop over shards and initialize each shard's map
|
|
for i := 0; i < shards; i++ {
|
|
c.s[i].m = make(map[string]int64)
|
|
}
|
|
return c
|
|
}
|
|
func (c *ShardedCounter) Inc(key string) {
|
|
// find shard for the key
|
|
hashF := fnv.New32a()
|
|
hashF.Write([]byte(key))
|
|
shard := hashF.Sum32() % shards
|
|
|
|
c.s[shard].mu.Lock()
|
|
defer c.s[shard].mu.Unlock()
|
|
// fmt.Println("shard", shard, "key", key, "value", c.s[shard].m[key])
|
|
c.s[shard].m[key]++
|
|
}
|
|
func (c *ShardedCounter) Get(key string) int64 {
|
|
// find shard for the key
|
|
hashF := fnv.New32a()
|
|
hashF.Write([]byte(key))
|
|
shard := hashF.Sum32() % shards
|
|
|
|
c.s[shard].mu.Lock()
|
|
defer c.s[shard].mu.Unlock()
|
|
return c.s[shard].m[key]
|
|
}
|
|
func (c *ShardedCounter) Snapshot() map[string]int64 {
|
|
out := make(map[string]int64)
|
|
for i := 0; i < shards; i++ {
|
|
c.s[i].mu.Lock()
|
|
for k, v := range c.s[i].m {
|
|
out[k] = v
|
|
}
|
|
c.s[i].mu.Unlock()
|
|
}
|
|
return out
|
|
}
|
|
|
|
func main() {
|
|
c := NewMutexCounter()
|
|
c.Inc("requests")
|
|
c.Inc("requests")
|
|
fmt.Println(c.Get("requests"), c.Snapshot())
|
|
}
|