sync.Map相当于goroutine并发安全的Map。
sync.Map针对两种常见情况进行了优化
- 给定key的entry只写一次,但读取多次时。比如缓冲增长
- 当多个goroutine对不相交key集进行读写覆盖
sync.Map在第一次使用后不能被复制
过程展现
-
最开始写入数据只会到dirty中,因为dirty主要接受写数据。此时read amended为true
-
读取时从read读取,此时read并没有数据,misses记录从read读取失败的次数,当misses大于等于dirty数据量时,将dirty升级为read。
升级也就是对dirty进行地址拷贝并且dirty清空,misses置为0
此时read amended为false
-
修改元素会直接修改read。此时read amended为false
-
新加元素,此时会直接操作dirty。从read复制元素到dirty且添加新元素到dirty。此时read amended为true
结构
type Map struct {
mu Mutex
// read包含map中可以安全并发读取的部分内容
// 读取read字段本身是安全的,但存储时必须持有mu
// 存储在read中的entry可以在没有mu的情况下并发更新,但更新一个已清除的entry时,需要将该数据项复制到脏map中,并在保持mu的情况下不清除。
// 只读数据,支持并发数据。若涉及到更新操作,则需要加锁来保证数据安全
read atomic.Value // readOnly
// dirty包含了map中需要保存mu的部分内容。为确保dirty map可以快速提升到read map,它还包括read map中所有未清除的项。
// expunged项不会存储在dirty中。在将新值存储到dirty的该key位置之前,clean map中的expunged项必须恢复并添加到dirty中。
// 如果dirty为nil,下一次对map的写入将通过创建clean map的副本来初始化它,并省略陈旧的元素。
dirty map[interface{}]*entry
// misses记录自从dirty提升为read以来dirty的访问(读取、删除、更新)次数
// 一旦misses达到dirty数据量,dirty就会被提升到read(处于未修改状态),然后下一个存储会创建一个新的dirty。
misses int
}
type readOnly struct {
m map[interface{}]*entry
// 标记dirty是否储存read没有的数据
amended bool // true if the dirty map contains some key not in m.
}
// read、dirty各自维护一套key,指向同一个value。也就是说只要修改了这个entry,对read、dirty都是可见的
// p有nil、expunged、正常 3种状态
// - nil: 代表该key-value被删除了,dirty为nil或者dirty[key]是e
// - expunged: 代表被删除,但此key只在read而没有在dirty中。出现在将read复制到dirty,复制过程中会先将read该key由nil标记为expunged,然后不将其复制到dirty
// - 正常: 存在数据
type entry struct {
p unsafe.Pointer // *interface{}
}
Store
func (m *Map) Store(key, value interface{}) {
// 若readMap中存在该key,则直接尝试修改(由于修改的是 entry 内部的 pointer,因此 dirty map 也可见)
read, _ := m.read.Load().(readOnly)
if e, ok := read.m[key]; ok && e.tryStore(&value) {
return
}
m.mu.Lock()
read, _ = m.read.Load().(readOnly)
if e, ok := read.m[key]; ok {
if e.unexpungeLocked() {
// entry之前是expunged,暗示有不含该entry的非nil dirty map
m.dirty[key] = e
}
e.storeLocked(&value)
} else if e, ok := m.dirty[key]; ok {
e.storeLocked(&value)
} else {
if !read.amended {
m.dirtyLocked()
// readOnly是结构,更新只能重新Store
m.read.Store(readOnly{m: read.m, amended: true})
}
m.dirty[key] = newEntry(value)
}
m.mu.Unlock()
}
-
检查read是否存在该插入元素key,若存在且没有标记删除状态,则尝试更新read取出的entry
-
加锁。再次检查read是否存在元素,若read存在
在取出entry为expunged情况下,就插入新元素到dirty,并更新read取出的entry
- 将从read取出的entry状态从expunged改为nil成功后(标记为删除,则说明dirty不等于nil且dirty不存在该值),将元素插入dirty
- 更新entry值为插入值
-
若read不存在该元素,但dirty存在,则直接写入更新entry
-
若发现read、dirty都不存在该key,则
dirty、read一致时就复制read到dirty,然后dirty再插入新元素
-
若amended记录dirty不存在read不存在的元素则
复制read到非nil dirty中,amended置为true
-
dirty插入插入元素
-
Load
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()
// Avoid reporting a spurious miss if m.dirty got promoted while we were
// blocked on m.mu. (If further loads of the same key will not miss, it's
// not worth copying the dirty map for this key.)
read, _ = m.read.Load().(readOnly)
e, ok = read.m[key]
if !ok && read.amended {
e, ok = m.dirty[key]
// Regardless of whether the entry was present, record a miss: this key
// will take the slow path until the dirty map is promoted to the read
// map.
m.missLocked()
}
m.mu.Unlock()
}
if !ok {
return nil, false
}
return e.load()
}
-
检查read是否包含所需元素,若有,则通过原子操作读取数据并返回
-
判断dirty是否包含read中没有的数据,若存在则会加锁,先到read,然后到dirty查找数据。
misses加一,当misses大于等于dirty数据量,则保存dirty到read,并置misses为0,dirty为nil
-
其他情况则找不到
LoadAndDelete
func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool) {
read, _ := m.read.Load().(readOnly)
e, ok := read.m[key]
if !ok && read.amended {
m.mu.Lock()
read, _ = m.read.Load().(readOnly)
e, ok = read.m[key]
if !ok && read.amended {
e, ok = m.dirty[key]
delete(m.dirty, key)
// Regardless of whether the entry was present, record a miss: this key
// will take the slow path until the dirty map is promoted to the read
// map.
m.missLocked()
}
m.mu.Unlock()
}
if ok {
return e.delete()
}
return nil, false
}
func (e *entry) delete() (value interface{}, ok bool) {
for {
p := atomic.LoadPointer(&e.p)
if p == nil || p == expunged {
return nil, false
}
if atomic.CompareAndSwapPointer(&e.p, p, nil) {
return *(*interface{})(p), true
}
}
}
- 尝试从read获取值。若不存在且dirty存在read不存在值时
- 加锁。再从read双重锁读取,在仍然不存在且dirty存在read不存在值时,删除dirty对应值。并记录misses加1,且尝试升级dirty
- 若能找到,则
- entry若是nil或者expunged,则返回说明已经删除了
- 尝试CAS更新状态为nil,若更新成功,则返回。不行则一直遍历,直到成功为止
显然可知如果几乎没有什么读的情况,将导致key、value在read中无法删除。并且read中的key也不会被删除
Ref
- https://pkg.go.dev/sync#Map
- https://segmentfault.com/a/1190000040729053
- https://juejin.cn/post/6844903618525528077
- https://studygolang.com/articles/30385