zoukankan      html  css  js  c++  java
  • 浅析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,调试输出如下

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

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

    接下来看更新操作

    依旧只会更新dirty数据

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

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

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

     

    到第三次的时候,misses < len(dirty)了,触发read升级,read更新了dirty的数据,amendedfalse说明readdirty的数据是一致的,dirty被置为nilmisses被置为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的线程安全

  • 相关阅读:
    34、JS/AJAX
    33、mybatis(二)
    32、mybatis
    31、springmvc(注解)
    30、springmvc
    29、Oralce(五)
    Spring学习之路-SpringBoot简单入门
    Spring学习之路-从放弃到入门
    心情日记
    Spring学习之路-从入门到放弃
  • 原文地址:https://www.cnblogs.com/zhp-king/p/15144620.html
Copyright © 2011-2022 走看看