Analysis of Go's sync.Map Implementation: Design, Architecture, and Source Code Walkthrough
The article explains how Go’s sync.Map achieves high‑concurrency performance by maintaining a lock‑free read‑only snapshot in an atomic.Value and a mutex‑protected dirty map, detailing entry state transitions, miss‑driven promotion, and the Store, Load, and Delete operations that together avoid a global lock.
Background: In high‑concurrency scenarios the native Go map is not safe for concurrent reads/writes, leading to panic. Two solutions are presented: using a sync.Mutex / sync.RWMutex (simple but slower) or using sync.Map , introduced in Go 1.9.
The article analyzes why sync.Map can achieve high performance without a global lock, by combining atomic operations and mutexes.
Core Idea & Architecture
sync.Map maintains two internal maps: a read‑only map ( read ) and a dirty map ( dirty ). The read map is stored in an atomic.Value and can be accessed without locking, providing fast reads. The dirty map holds all keys that are not yet present in the read map and is protected by a sync.Mutex .
Read map: snapshot, accessed atomically, may not contain newly added keys.
Dirty map: full key set, protected by lock, used when a key is missing from the read map.
Miss counter triggers promotion of dirty to read when reads miss frequently.
Entries are wrapped in an entry struct whose p field can be nil , expunged , or a normal pointer. This allows logical deletion without immediate removal.
Key Operations
Store (C/U)
func (m *Map) Store(key, value interface{}) {
read, _ := m.read.Load().(readOnly)
if e, ok := read.m[key]; ok && e.tryStore(&value) {
return
}
m.mu.Lock()
// ... handle read, dirty, create new entry, etc.
m.mu.Unlock()
}
func (e *entry) tryStore(i *interface{}) bool { /* CAS logic */ }
func (e *entry) unexpungeLocked() (wasExpunged bool) { /* CAS */ }
func (e *entry) storeLocked(i *interface{}) { /* atomic store */ }
func (m *Map) dirtyLocked() { /* initialize dirty from read */ }
func (e *entry) tryExpungeLocked() (isExpunged bool) { /* turn nil into expunged */ }Load (R)
func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
read, _ := m.read.Load().(readOnly)
e, ok := read.m[key]
if !ok && read.amended {
m.mu.Lock()
// ... check dirty, increment miss counter
m.mu.Unlock()
}
if !ok {
return nil, false
}
return e.load()
}
func (m *Map) missLocked() { /* promote dirty to read when misses exceed threshold */ }
func (e *entry) load() (value interface{}, ok bool) { /* atomic load */ }Delete (D)
func (m *Map) Delete(key interface{}) {
m.LoadAndDelete(key)
}
func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool) {
// similar to Load, then remove entry
}
func (e *entry) delete() (value interface{}, ok bool) { /* CAS to nil */ }The article also discusses state transitions of entry.p (nil → expunged, normal pointer), the conditions for read‑to‑dirty and dirty‑to‑read promotion, and the rationale behind having both nil and expunged states.
Overall Thoughts
sync.Map ’s design cleverly balances lock‑free reads with occasional locked writes, using atomic snapshots and a dirty map to minimise contention while preserving correctness. Understanding these mechanisms helps developers choose the right concurrency primitive for high‑throughput Go services.
Tencent Cloud Developer
Official Tencent Cloud community account that brings together developers, shares practical tech insights, and fosters an influential tech exchange community.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.