Go 的内建 map
是不支持并发写操做的,缘由是 map
写操做不是并发安全的,当你尝试多个 Goroutine 操做同一个 map
,会产生报错:fatal error: concurrent map writes
。git
所以官方另外引入了 sync.Map
来知足并发编程中的应用。github
sync.Map
的实现原理可归纳为:golang
Map
的数据结构以下:编程
type Map struct { // 加锁做用,保护 dirty 字段 mu Mutex // 只读的数据,实际数据类型为 readOnly read atomic.Value // 最新写入的数据 dirty map[interface{}]*entry // 计数器,每次须要读 dirty 则 +1 misses int }
其中 readOnly 的数据结构为:安全
type readOnly struct { // 内建 map m map[interface{}]*entry // 表示 dirty 里存在 read 里没有的 key,经过该字段决定是否加锁读 dirty amended bool }
entry
数据结构则用于存储值的指针:数据结构
type entry struct { p unsafe.Pointer // 等同于 *interface{} }
属性 p 有三种状态:并发
p == nil
: 键值已经被删除,且 m.dirty == nil
p == expunged
: 键值已经被删除,但 m.dirty!=nil
且 m.dirty
不存在该键值(expunged 实际是空接口指针)m.read.m
中,若是 m.dirty!=nil
则也存在于 m.dirty
Map
经常使用的有如下方法:函数
Load
:读取指定 key 返回 valueStore
: 存储(增或改)key-valueDelete
: 删除指定 keyfunc (m *Map) Load(key interface{}) (value interface{}, ok bool) { // 首先尝试从 read 中读取 readOnly 对象 read, _ := m.read.Load().(readOnly) e, ok := read.m[key] // 若是不存在则尝试从 dirty 中获取 if !ok && read.amended { m.mu.Lock() // 因为上面 read 获取没有加锁,为了安全再检查一次 read, _ = m.read.Load().(readOnly) e, ok = read.m[key] // 确实不存在则从 dirty 获取 if !ok && read.amended { e, ok = m.dirty[key] // 调用 miss 的逻辑 m.missLocked() } m.mu.Unlock() } if !ok { return nil, false } // 从 entry.p 读取值 return e.load() } func (m *Map) missLocked() { m.misses++ if m.misses < len(m.dirty) { return } // 当 miss 积累过多,会将 dirty 存入 read,而后 将 amended = false,且 m.dirty = nil m.read.Store(readOnly{m: m.dirty}) m.dirty = nil m.misses = 0 }
func (m *Map) Store(key, value interface{}) { read, _ := m.read.Load().(readOnly) // 若是 read 里存在,则尝试存到 entry 里 if e, ok := read.m[key]; ok && e.tryStore(&value) { return } // 若是上一步没执行成功,则要分状况处理 m.mu.Lock() read, _ = m.read.Load().(readOnly) // 和 Load 同样,从新从 read 获取一次 if e, ok := read.m[key]; ok { // 状况 1:read 里存在 if e.unexpungeLocked() { // 若是 p == expunged,则须要先将 entry 赋值给 dirty(由于 expunged 数据不会留在 dirty) m.dirty[key] = e } // 用值更新 entry e.storeLocked(&value) } else if e, ok := m.dirty[key]; ok { // 状况 2:read 里不存在,但 dirty 里存在,则用值更新 entry e.storeLocked(&value) } else { // 状况 3:read 和 dirty 里都不存在 if !read.amended { // 若是 amended == false,则调用 dirtyLocked 将 read 拷贝到 dirty(除了被标记删除的数据) m.dirtyLocked() // 而后将 amended 改成 true m.read.Store(readOnly{m: read.m, amended: true}) } // 将新的键值存入 dirty m.dirty[key] = newEntry(value) } m.mu.Unlock() } func (e *entry) tryStore(i *interface{}) bool { for { p := atomic.LoadPointer(&e.p) if p == expunged { return false } if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) { return true } } } func (e *entry) unexpungeLocked() (wasExpunged bool) { return atomic.CompareAndSwapPointer(&e.p, expunged, nil) } func (e *entry) storeLocked(i *interface{}) { atomic.StorePointer(&e.p, unsafe.Pointer(i)) } func (m *Map) dirtyLocked() { if m.dirty != nil { return } read, _ := m.read.Load().(readOnly) m.dirty = make(map[interface{}]*entry, len(read.m)) for k, e := range read.m { // 判断 entry 是否被删除,不然就存到 dirty 中 if !e.tryExpungeLocked() { m.dirty[k] = e } } } func (e *entry) tryExpungeLocked() (isExpunged bool) { p := atomic.LoadPointer(&e.p) for p == nil { // 若是有 p == nil(即键值对被 delete),则会在这个时机被置为 expunged if atomic.CompareAndSwapPointer(&e.p, nil, expunged) { return true } p = atomic.LoadPointer(&e.p) } return p == expunged }
func (m *Map) Delete(key interface{}) { m.LoadAndDelete(key) } // LoadAndDelete 做用等同于 Delete,而且会返回值与是否存在 func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool) { // 获取逻辑和 Load 相似,read 不存在则查询 dirty 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] m.missLocked() } m.mu.Unlock() } // 查询到 entry 后执行删除 if ok { // 将 entry.p 标记为 nil,数据并无实际删除 // 真正删除数据并被被置为 expunged,是在 Store 的 tryExpungeLocked 中 return e.delete() } return nil, false }
可见,经过这种读写分离的设计,解决了并发状况的写入安全,又使读取速度在大部分状况能够接近内建 map
,很是适合读多写少的状况。atom
sync.Map
还有一些其余方法:设计
Range
:遍历全部键值对,参数是回调函数LoadOrStore
:读取数据,若不存在则保存再读取这里就再也不详解了,可参见 源码。