ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

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

2021-08-15 20:31:07  阅读:203  来源: 互联网

标签:Map ok read goroutine readOnly dirty key 浅析


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的线程安全

 

标签:Map,ok,read,goroutine,readOnly,dirty,key,浅析
来源: https://www.cnblogs.com/zhp-king/p/15144620.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有