1、golang内置Map问题
Golang内置的Map数据类型,在遇到并发的时候,可能会抛出异常
fatal error: concurrent map read and map write
而官方的解决方案就是使用sync.Map来解决改问题,那么话不多说,接下来通过源码分析,sync.Map是如何解决goroutine安全的呢?
2、sync.Map源码分析
Sync.Map 的源码是在$GOPATH/src/sync/map.go下,我们先来看下sync.Map的结构
type Map struct { // 涉及到dirty数据的操作,会使用这个锁 mu Mutex // map的所有读取,都是通过这个数据结构的 read atomic.Value // readOnly // map的所有更新操作(包括增删改),都是通过这个数据结构的 dirty map[interface{}]*entry // 记录从read atomic.Value中无法读取到数据的次数 misses int }
在这之前,我们还需要了解两个数据结构,那就是readOnly 和 entry
type readOnly struct { // read atomic.Value存储的值 m map[interface{}]*entry // dirty和read的数据有差异,就为true amended bool } type entry struct { // 真实存放map的数据 // If p == nil,那么就说明值已经被删除 // If p == expunged,则说明被标记为已删除了 // 其他情况就是p指向的是正常的数据 p unsafe.Pointer // *interface{} }
那么,了解了map的结构之后,我们先来看看读取数据方法
func (m *Map) Load(key interface{}) (value interface{}, ok bool) { // 从这行代码中,我们可以得知read atomic.Value读取出来的readOnly结构,在readOnly的map中查找数据 read, _ := m.read.Load().(readOnly) e, ok := read.m[key] // 如果没找到,而且dirty中有新数据,那么就加锁查找dirty if !ok && read.amended { m.mu.Lock() read, _ = m.read.Load().(readOnly) e, ok = read.m[key] // 双检查,目的是为了在加锁期间,检查read atomic.Value中已经更新数据(因为上一步中的读取和加锁并非原子性的,所以此处需要校验) if !ok && read.amended { // 读取dirty中记录的数据 e, ok = m.dirty[key] // 检验read atomic.Value是否需要用dirty的来覆盖 m.missLocked() } m.mu.Unlock() } if !ok { return nil, false } return e.load() }
再来,就是看看出现在load中出现的missLocked方法
func (m *Map) missLocked() { // 每次去dirty去查找数据的时候,misses值加1 m.misses++ // 如果misses值小于dirty的长度,不执行任何操作,否则,用dirty的数据更新掉read的数据,然后重置dirty 和misses ,进行新的一轮计数 if m.misses < len(m.dirty) { return } m.read.Store(readOnly{m: m.dirty}) m.dirty = nil m.misses = 0 }
接下来我们继续看Store方法
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() read, _ = m.read.Load().(readOnly) // 还是双检查 if e, ok := read.m[key]; ok { // 先标记为已删除 if e.unexpungeLocked() { // 如果read中被标记为已删除,那么dirty需要把这个值加回来 m.dirty[key] = e } // 更新 e.storeLocked(&value) } else if e, ok := m.dirty[key]; ok { // 如果dirty中key存在,直接更新 e.storeLocked(&value) } else { // 如果dirty和read数据不一致 if !read.amended { // 如果dirty为nil,则把read赋值给dirty(新map或者是把dirty赋值给read的时候,dirty就为nil) m.dirtyLocked() // 标记dirty和read已经有差异了 m.read.Store(readOnly{m: read.m, amended: true}) } // 新数据加入到dirty里面 m.dirty[key] = newEntry(value) } m.mu.Unlock() }
然后看看在store出现的dirtyLocked方法,源码如下
func (m *Map) dirtyLocked() { //如果dirty不为nil,就不需要用read来覆盖了 if m.dirty != nil { return } // 遍历read,把每个值赋给dirty read, _ := m.read.Load().(readOnly) m.dirty = make(map[interface{}]*entry, len(read.m)) for k, e := range read.m { if !e.tryExpungeLocked() { m.dirty[k] = e } }
}
接下来,我们看看delete方法
func (m *Map) Delete(key interface{}) { 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 { delete(m.dirty, key) } m.mu.Unlock() } if ok { e.delete() } }
Delete方法比较简单,其实就是删除dirty的数据,或者是当amended为false,也就是dirty和read数据一致的时候,会直接把read对应的entry中的p置为nil
接下来到最后一个方法range,看到这边博客的小伙伴们可以尝试着自己去看下,这个方法最值得一提的是每次调用,它都会用dirty来覆盖dirty的值。源码如下
func (m *Map) Range(f func(key, value interface{}) bool) { // We need to be able to iterate over all of the keys that were already // present at the start of the call to Range. // If read.amended is false, then read.m satisfies that property without // requiring us to hold m.mu for a long time. read, _ := m.read.Load().(readOnly) if read.amended { // m.dirty contains keys not in read.m. Fortunately, Range is already O(N) // (assuming the caller does not break out early), so a call to Range // amortizes an entire copy of the map: we can promote the dirty copy // immediately! m.mu.Lock() read, _ = m.read.Load().(readOnly) 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 } } }
3、dlv工具调试,验证结果
sync.Map源码调试,进行验证(这里用的是dlv工具),测试代码如下
func main() { m:=sync.Map{} m.Store(1,"a") m.Store(2,"b") m.Store(3,"c") // 更新操作 m.Store(1, "e") m.Store(3, "f") // 读取操作 m.Load(1) m.Load(1) m.Load(1) // 更新操作 m.Store(2, "k") // 删除操作 m.Delete(2) fmt.Println("调试完成") }
首先创建symc.Map,调试输出如下
然后添加完3个值之后的输出
可以看到,每次添加的时候,只会往dirty插入数据,并不会往read插入数据,而且amended标记为true,证明read和dirty已经不一致了
接下来看更新操作
依旧只会更新dirty数据
接下来就是读取操作了,dlv进入load方法进行追踪,在read找不到值的时候,就会去dirty去找
读取完数值之后,此时会发现,read依然没有数据,但是misses的值却增加了
之后再读取数据,misses的值再次增加
到第三次的时候,misses < len(dirty)了,触发read升级,read更新了dirty的数据,amended为false说明read和dirty的数据是一致的,dirty被置为nil,misses被置为0,从头开始计算
接下来再更新一条数据
注意,正如上文所说,这里是分为多种情况的,这里只演示其中一种情况,其他的情况看到该博客的小伙伴们可以自己动手去尝试下
因为m.read存在这个键,并且这个entry这个元素没有被标记删除,所以会直接尝试直接存储,也就是走到这个地方
调试输出结果如下
最后是删除(删除也是分情况的,这里也只演示一种,read存在key且amended为false),也就是走到这个地方
最后调试结果如下,把p置为了nil
4、结论
- sync.Map在读取的时候,会从read中获取数据,找不到的时候会去dirty查询
- sync.Map在更新的时候,只会更新dirty,在更新dirty的时候会加锁,当misses>=len(dirty)的时候,会触发read升级操作,把dirty的值更新到read中
基于以上策略,来保证map的线程安全