浅析sync.Map是如何解决goroutine安全

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,调试输出如下

浅析sync.Map是如何解决goroutine安全

 

然后添加完3个值之后的输出

浅析sync.Map是如何解决goroutine安全

可以看到,每次添加的时候,只会往dirty插入数据,并不会往read插入数据,而且amended标记为true,证明read和dirty已经不一致了

 

接下来看更新操作

浅析sync.Map是如何解决goroutine安全

依旧只会更新dirty数据

 

接下来就是读取操作了,dlv进入load方法进行追踪,在read找不到值的时候,就会去dirty去找

浅析sync.Map是如何解决goroutine安全

读取完数值之后,此时会发现,read依然没有数据,但是misses的值却增加了

浅析sync.Map是如何解决goroutine安全

之后再读取数据,misses的值再次增加

 浅析sync.Map是如何解决goroutine安全

到第三次的时候,misses < len(dirty)了,触发read升级,read更新了dirty的数据,amended为false说明read和dirty的数据是一致的,dirty被置为nil,misses被置为0,从头开始计算

浅析sync.Map是如何解决goroutine安全

 

接下来再更新一条数据

注意,正如上文所说,这里是分为多种情况的,这里只演示其中一种情况,其他的情况看到该博客的小伙伴们可以自己动手去尝试下

因为m.read存在这个键,并且这个entry这个元素没有被标记删除,所以会直接尝试直接存储,也就是走到这个地方

浅析sync.Map是如何解决goroutine安全

 

浅析sync.Map是如何解决goroutine安全

调试输出结果如下

浅析sync.Map是如何解决goroutine安全

 

最后是删除(删除也是分情况的,这里也只演示一种,read存在key且amended为false),也就是走到这个地方

浅析sync.Map是如何解决goroutine安全

 

 最后调试结果如下,把p置为了nil

 浅析sync.Map是如何解决goroutine安全

 

4、结论 

  •  sync.Map在读取的时候,会从read中获取数据,找不到的时候会去dirty查询
  •  sync.Map在更新的时候,只会更新dirty,在更新dirty的时候会加锁,当misses>=len(dirty)的时候,会触发read升级操作,把dirty的值更新到read中

基于以上策略,来保证map的线程安全

 

上一篇:QT-undefined reference to vtable


下一篇:创建新虚拟机CentOS 6