特别提醒,本文所涉及的源码是go1.21.0 darwin/amd64 文件位置:runtime/map.go
普通的map并不是并发安全的一个数据结构,因此想要使用并发安全的map,sync包下提供了一个sync.map数据结构
1 数据结构 1 2 3 4 5 6 7 type Map struct { mu Mutex read atomic.Pointer[readOnly] dirty map [any]*entry misses int }
read:无锁化的map,实际类型为atomic.Pointer
dirty:加锁的map,是一个普通的map
misses:每次从read中读取失败,misses都会加1,达到一定阈值后,会将dirty提升为read
1 2 3 4 type readOnly struct { m map [any]*entry amended bool }
m:read中的map,存放k-v对
amended:标识read和dirty中的元素是否一致
1 2 3 type entry struct { p atomic.Pointer[any] }
p:k-v对中的value,使用atomic.Pointer的形式,通过entry.p的指针来获取值
2 entry的几种状态 2.1 p==nil(软删除) 此时p是nil,m.dirty等于nil或者m.dirty[key]指向该entry
2.2 p==expunged(硬删除) 1 2 var expunged = new (any)
2.3 p指向正常值的地址 这种情况就是k-v对没删除的情况,可以通过m.read.m[key]找到,如果此时dirty也不为nil,同样也可以通过m.dirty[key]找到,两者实际指向的是同一个值。
3 Load 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 func (m *Map) Load(key any) (value any, ok bool ) { read := m.loadReadOnly() e, ok := read.m[key] if !ok && read.amended { m.mu.Lock() read = m.loadReadOnly() e, ok = read.m[key] if !ok && read.amended { e, ok = m.dirty[key] m.missLocked() } m.mu.Unlock() } if !ok { return nil , false } return e.load() }
首先会调用loadReadOnly函数获取read
1 2 3 4 5 6 7 func (m *Map) loadReadOnly() readOnly { if p := m.read.Load(); p != nil { return *p } return readOnly{} }
如果未初始化,则在该函数中初始化,否则直接返回m.read
通过key来读取read.m中的value,然后存在该元素则直接返回值就行,否则要上锁,再判断m中是否存在该key,如果还是不存在则去dirty中查找。
1 2 3 4 5 6 7 8 9 10 func (m *Map) missLocked() { m.misses++ if m.misses < len (m.dirty) { return } m.read.Store(&readOnly{m: m.dirty}) m.dirty = nil m.misses = 0 }
如果在read和dirty都没找到,则返回nil,如果找到了则调用load返回对应的值
1 2 3 4 5 6 7 8 9 func (e *entry) load() (value any, ok bool ) { p := e.p.Load() if p == nil || p == expunged { return nil , false } return *p, true }
4 Store 1 2 3 func (m *Map) Store(key, value any) { _, _ = m.Swap(key, value) }
Store函数是通过调用Swap函数来实现逻辑功能的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 func (m *Map) Swap(key, value any) (previous any, loaded bool ) { read := m.loadReadOnly() if e, ok := read.m[key]; ok { if v, ok := e.trySwap(&value); ok { if v == nil { return nil , false } return *v, true } } m.mu.Lock() read = m.loadReadOnly() if e, ok := read.m[key]; ok { if e.unexpungeLocked() { m.dirty[key] = e } if v := e.swapLocked(&value); v != nil { loaded = true previous = *v } } else if e, ok := m.dirty[key]; ok { if v := e.swapLocked(&value); v != nil { loaded = true previous = *v } } else { if !read.amended { m.dirtyLocked() m.read.Store(&readOnly{m: read.m, amended: true }) } m.dirty[key] = newEntry(value) } m.mu.Unlock() return previous, loaded }
1 2 3 4 5 6 7 8 9 10 11 12 13 func (e *entry) trySwap(i *any) (*any, bool ) { for { p := e.p.Load() if p == expunged { return nil , false } if e.p.CompareAndSwap(p, i) { return p, true } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 func (m *Map) dirtyLocked() { if m.dirty != nil { return } read := m.loadReadOnly() m.dirty = make (map [any]*entry, len (read.m)) for k, e := range read.m { if !e.tryExpungeLocked() { m.dirty[k] = e } } }
5 Delete 1 2 3 func (m *Map) Delete(key any) { m.LoadAndDelete(key) }
map的Delete操作都是调用LoadAndDelete来实现的,因此直接看它是怎么实现的即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 func (m *Map) LoadAndDelete(key any) (value any, loaded bool ) { read := m.loadReadOnly() e, ok := read.m[key] if !ok && read.amended { m.mu.Lock() read = m.loadReadOnly() e, ok = read.m[key] if !ok && read.amended { e, ok = m.dirty[key] delete (m.dirty, key) m.missLocked() } m.mu.Unlock() } if ok { return e.delete () } return nil , false }
1 2 3 4 5 6 7 8 9 10 11 12 13 func (e *entry) delete () (value any, ok bool ) { for { p := e.p.Load() if p == nil || p == expunged { return nil , false } if e.p.CompareAndSwap(p, nil ) { return *p, true } } }
为什么这里删除p的时候不是直接将其置为expunged而是置为nil呢?
因为在tryExpungeLocked函数中,会将nil设置为expunged
1 2 3 4 5 6 7 8 9 10 11 func (e *entry) tryExpungeLocked() (isExpunged bool ) { p := e.p.Load() for p == nil { if e.p.CompareAndSwap(nil , expunged) { return true } p = e.p.Load() } return p == expunged }
注意到如果 key 同时存在于read 和 dirty 中时,删除只是做了一个标记,将p 置为 nil;而如果仅在 dirty 中含有这个 key 时,会直接删除这个key。原因在于,若两者都存在这个 key,仅做标记删除,可以在下次查找这个key 时,命中 read,提升效率。若只有在dirty 中存在时,read 起不到“缓存”的作用,直接删除。
6 Range 由用户实现,Range将遍历map中的所有k-v对,将他们传给f函数,如果f返回false则停止遍历
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 func (m *Map) Range(f func (key, value any) bool ) { read := m.loadReadOnly() if read.amended { m.mu.Lock() read = m.loadReadOnly() if read.amended { read = readOnly{m: m.dirty} m.read.Store(&read) m.dirty = nil m.misses = 0 } m.mu.Unlock() } for k, e := range read.m { v, ok := e.load() if !ok { continue } if !f(k, v) { break } } }
7 总结 什么时候会出现expunge?
当key的值在 read 中为 nil,随后由其他操作触发read 向 dirty 复制,nil 被转变为expunge。此时 dirty 不为空,且key 在dirty 中不存在。