深入解析 go sync.map 源码

 在实际开发项目中,常常有并发操作map 的情况,但是go官方map 并不支持并发读写,这时候可能需要用到sync.map 了。在项目中其实sync.map 更适用于少写的情况,比如服务器启动注册某些东西到map里面,后面服务器启动后几乎是只读了。

自己面试亲身经历:

面试官:在实际生产中,可能会有并发写map 的情况,balabala.....

我:sync.map 可以解决这些事情

面试官:它的应用场景?

我:读多写少,源码,官方,性能测试都证明了

面试官:使用sync.map有没有遇到过问题?

我:没有,然后就没有然后了<( ̄ ﹌  ̄)> 。吐槽一句:正常用会出毛的问题,都是用在写一次,大量读场景,删除几乎都不用。

面试官: go 官方map 为什么不支持并发

我:懵逼φ(>ω<*) ,然后猝

1源码解析

1.1结构

Map结构

type Map struct {
   mu Mutex //互斥锁
​
   read atomic.Value // readOnly //读结构
​
   dirty map[interface{}]*entry  //写结构
​
   misses int //没有命中的次数
}

readOnly结构

type readOnly struct {
    m       map[interface{}]*entry
    amended bool // true 当dirty 里面有些字段没有在m里面
}
  • readOnly里面的m和dirty里面结构一样

entry 结构

type entry struct {
   p unsafe.Pointer // *interface{}
}
  • p 存储的值是value指针地址

1.2Load(获取值)

func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
   read, _ := m.read.Load().(readOnly)
   e, ok := read.m[key]
   if !ok && read.amended {//再加锁之前获取,然后加锁之后再获取,是防止加锁时,有人更改了该key,那么为什么不直接在锁里面获取呢,在go的官方库思想都是这样,加锁之前获取可以避免走锁,提高性能
      m.mu.Lock()
   
      read, _ = m.read.Load().(readOnly)
      e, ok = read.m[key]
      if !ok && read.amended {//没有找到key并且read.amended为true,就将值存在dirty里面
         e, ok = m.dirty[key]
         
         m.missLocked()
      }
      m.mu.Unlock()
   }
   if !ok {
      return nil, false
   }
   return e.load()
}
  • 步骤1: 从read里面原子加载readonly,然后从read.m 结构获取key对应得value 值

read, _ := m.read.Load().(readOnly)
   e, ok := read.m[key]
  • 步骤2: 如果从read 里面没有获取到值,并且read.amended为true,说明dirty里有read里面没有的值,去dirty里面找

if !ok && read.amended {
      m.mu.Lock() //加锁,因为m.dirty 是普通map,有并发安全
 
      read, _ = m.read.Load().(readOnly) // 再从read 里面获取下,因为加锁时有可能其他协程操作,里面又有数据了
      e, ok = read.m[key]
      if !ok && read.amended {
         e, ok = m.dirty[key] // 再次没找到,从dirty 里面获取key
         
         m.missLocked()
      }
      m.mu.Unlock() //解锁
   }

missLocked

func (m *Map) missLocked() { //在从dirty 中获取值的时候,将没有命中次数+1
    m.misses++
    if m.misses < len(m.dirty) { //如果命中次数< dirty 的长度返回
        return
    }
    m.read.Store(readOnly{m: m.dirty}) //如果没有命中read 次数超过dirty 的长度,将dirty 置为nil,命中次数置为0
    m.dirty = nil
    m.misses = 0
}
  • 步骤3: 如果都没找到,ok=false,返回没有找到

  • 步骤4: 如果找到,ok =true ,返回entry.load()

entry.load

func (e *entry) load() (value interface{}, ok bool) {
   p := atomic.LoadPointer(&e.p)
   if p == nil || p == expunged { //当删除的时候,先标记为nil,然后store的时候,如果操作dirtymap,再将p 标记为expunged
      return nil, false
   }
   return *(*interface{})(p), true
}
  • 该函数主要是取entry里面的值,先原子获取e.p 的地址,如果p==nil 或者 p==expunged 则返回没找到,如果有值则返回一个接口值

1.3store

// Store sets the value for a key.
func (m *Map) Store(key, value interface{}) {
   read, _ := m.read.Load().(readOnly)
   if e, ok := read.m[key]; ok && e.tryStore(&value) {//如果read有值,存储成功就返回true
      return
   }
  //来到这里说明read要么是没值,要么是值被标记删除了
   m.mu.Lock()
   read, _ = m.read.Load().(readOnly)
   if e, ok := read.m[key]; ok {
      if e.unexpungeLocked() {//这里可能有个疑问, m.dirty是否被置为nil?答案肯定是不可能的,unexpungeLocked为true的条件是执行了m.dirtyLocked(),说明m.dirty现在里面是所有未被删除的值
         // The entry was previously expunged, which implies that there is a
         // non-nil dirty map and this entry is not in it.
         m.dirty[key] = e
      }
      e.storeLocked(&value)
   } else if e, ok := m.dirty[key]; ok {//如果从dirty里面找到值,直接覆盖就行了
      e.storeLocked(&value)
   } else { //如果上面两种都没找到,说明没有这个key
      if !read.amended {//
         // We're adding the first new key to the dirty map.
         // Make sure it is allocated and mark the read-only map as incomplete.
         m.dirtyLocked()
         m.read.Store(readOnly{m: read.m, amended: true})
      }
      m.dirty[key] = newEntry(value)//创建一个新的entry赋值给dirty
   }
   m.mu.Unlock()
}
  • 首先从read里面获取readonly,如果存在则调用tryStore 存储

  • 加锁的时候如果再一次成功的获取到了key ,说明已经有值,并且之前被标记为删除,将被删除的值重新置为nil,如果成功了,再将key 和value赋值到dirty

read, _ = m.read.Load().(readOnly)
   if e, ok := read.m[key]; ok {
      if e.unexpungeLocked() {
         // The entry was previously expunged, which implies that there is a
         // non-nil dirty map and this entry is not in it.
         m.dirty[key] = e
      }
      e.storeLocked(&value)
   }
  • dirtyLocked下面有讲作用,是将read里面未被删除的值循环拷贝到新的dirty里面去,然后再将旧的readOnly重新放入m.read里面,amended设置为true,所以这个标志,代表dirty 里面有read里面没有的值。

m.dirtyLocked()
m.read.Store(readOnly{m: read.m, amended: true})

unexpungeLocked

// unexpungeLocked ensures that the entry is not marked as expunged.
//
// If the entry was previously expunged, it must be added to the dirty map
// before m.mu is unlocked.
func (e *entry) unexpungeLocked() (wasExpunged bool) {
    return atomic.CompareAndSwapPointer(&e.p, expunged, nil)
}
  • 判断是否被删除的同时,将expunged原子赋值为nil,如果原来被删除过了,那么将这个值赋值给dirty

storeLocked

// The entry must be known not to be expunged.
func (e *entry) storeLocked(i *interface{}) {
   atomic.StorePointer(&e.p, unsafe.Pointer(i))
}

将传入的值原子存储e.p

dirtyLocked

func (m *Map) dirtyLocked() {
   if m.dirty != nil { //如果dirty为nil直接返回,说明刚执行完m.misslocked,将dirty拷贝到read
      return
   }
​
   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
      }
   }
}
​
//该函数作用是循环判断value,是不是被删除的,当元素被删除的时候,会被置为nil
func (e *entry) tryExpungeLocked() (isExpunged bool) {
    p := atomic.LoadPointer(&e.p)
    for p == nil {//如果p ==nil,那么说明该元素刚被删除,那么就将nil置为 expunged,然后返回true
        if atomic.CompareAndSwapPointer(&e.p, nil, expunged) {
            return true
        }
        p = atomic.LoadPointer(&e.p)
    }
    return p == expunged
}
  • 从read里面加载存储的map,循环遍历里面的k和v,重新创建dirty,然后将read里面没有被删除的值重新加入到dirty里面去

tryStore

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
      }
   }
}
  • 原子获取entry 的值,如果 获取到的值被标记为删除,那么返回false,代表存储失败,如果成功只直接用cas 原语将值赋值给e.p

1.4LoadOrStore

func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool) {
   // Avoid locking if it's a clean hit.
   read, _ := m.read.Load().(readOnly)
   if e, ok := read.m[key]; ok {
      actual, loaded, ok := e.tryLoadOrStore(value)
      if ok { //如果成功了,就返回 actual, loaded
         return actual, loaded
      }
   }
​
   m.mu.Lock()
   read, _ = m.read.Load().(readOnly)//上面不ok只有一种情况,那就是e.p == expunged, 所以下面再走下存储流程,跟store流程相似
   if e, ok := read.m[key]; ok {
      if e.unexpungeLocked() {
         m.dirty[key] = e
      }
      actual, loaded, _ = e.tryLoadOrStore(value)
   } else if e, ok := m.dirty[key]; ok {
      actual, loaded, _ = e.tryLoadOrStore(value)
      m.missLocked()
   } else {
      if !read.amended {
         // We're adding the first new key to the dirty map.
         // Make sure it is allocated and mark the read-only map as incomplete.
         m.dirtyLocked()
         m.read.Store(readOnly{m: read.m, amended: true})
      }
      m.dirty[key] = newEntry(value)
      actual, loaded = value, false
   }
   m.mu.Unlock()
​
   return actual, loaded
}
  • 总结来说该函数作用就是获取key现在的值,如果现在没有值就把新值设置进去,loaded为true表明值是里面原来的值,为false表面原来里面没有值是新添加进去的

tryLoadOrStore

func (e *entry) tryLoadOrStore(i interface{}) (actual interface{}, loaded, ok bool) {
   p := atomic.LoadPointer(&e.p)
   if p == expunged { //先走entry里面加载值,如果为被删除的值,直接返回nil,false,false
      return nil, false, false
   }
   if p != nil {//如果有值直接返回原来的值
      return *(*interface{})(p), true, true
   }
​
   // Copy the interface after the first load to make this method more amenable
   // to escape analysis: if we hit the "load" path or the entry is expunged, we
   // shouldn't bother heap-allocating.
   ic := i
    for {
        //如果e.p =nil,就将新值赋值给e.p
      if atomic.CompareAndSwapPointer(&e.p, nil, unsafe.Pointer(&ic)) {
         return i, false, true
      }
      p = atomic.LoadPointer(&e.p)
      if p == expunged {  //如果赋新值没成功,并且p 被标记为expunged,那么返回false
         return nil, false, false
      }
      if p != nil {
         return *(*interface{})(p), true, true //如果赋新值没成功,但是e.p 是有效的,就返回现在的值
      }
   }
}
  • 就是将e.p 的值返回,如果为expunged则返回nil,如果为nil,循环赋值,直到e.p不为nil。

  • 返回参数loaded和ok当里面额e.p有值得时候返回true,如果e.p 原来为nil,但是赋值新值成功,loaded为false,ok 为true

1.5Delete

// Delete deletes the value for a key.
func (m *Map) Delete(key interface{}) {
   m.LoadAndDelete(key)
}
  • 该方法调用LoadAndDelete进行删除值

LoadAndDelete

// LoadAndDelete deletes the value for a key, returning the previous value if any.
// The loaded result reports whether the key was present.
func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool) {
    read, _ := m.read.Load().(readOnly)
    e, ok := read.m[key] //先从read里面获取值
    if !ok && read.amended {//如果read里面没有值,并且read.amended有值,则走下面的逻辑
        m.mu.Lock()
        read, _ = m.read.Load().(readOnly)
        e, ok = read.m[key]
        if !ok && read.amended {
            e, ok = m.dirty[key]
            delete(m.dirty, key)
            // Regardless of whether the entry was present, record a miss: this key
            // will take the slow path until the dirty map is promoted to the read
            // map.
            m.missLocked()
        }
        m.mu.Unlock()
    }
    if ok {//获取到值了执行entry.delete 方法
        return e.delete()
    }
    return nil, false
}
​
  • 先从read里面加载值,如果获取到,走e.delete逻辑

  • 如果没有获取到,并且read.amended为true,那么则从dirty里面删除,删除dirty是直接将dirty map里面的值删除的,最后执行missLocked,然后再进行entry.delete,获取原来的值,并且置为nil

entry.delete

func (e *entry) delete() (value interface{}, ok bool) {
   for {
      p := atomic.LoadPointer(&e.p)
      if p == nil || p == expunged {
         return nil, false
      }
      if atomic.CompareAndSwapPointer(&e.p, p, nil) {
         return *(*interface{})(p), true
      }
   }
}
  • 如果entry里面的值为nil,或者为标记删除,那么直接返回nil,false,如果有值,那么将e.p置为nil,返回原来的值

1.6Range

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) {//如果回调函数返回false直接退出循环,相当于控制循环结束
         break
      }
   }
}
  • range的步骤是先从read里面去加载readOnly,然后如果获取到的read.amended是true,就把dirty赋值给read,然后将dirty置为nil,最后再调用传进来的回调函数,把key,value 传进去

总结

  • 存储流程:

    先从read里面获取值,如果获取到值,并且tryStore成功就直接返回,tryStore不成功说明值已经被标记为了expunged了。什么时候被标记为expunged呢? 答案就是在存储的时候,read和dirty没有key,并且没有执行过dirtyLocked,dirtyLocked会将read里面有效的值赋值给dirty,将被删除标记为nil的值,标记expunged。

    tryStore 失败 代表值可能不存在,可能是存在被标记为删除,走加锁逻辑,在锁里面再获取read逻辑,如果值存在。分两种情况:一种是里面有值了,但不是expunged,说明有其他协程在加锁前操作了,给赋值了,

    那么直接覆盖掉值就好。另一种是值还是expunged,此时存储在dirty里面就可以了。如果值不存在就从dirty里面试试,有值的话直接覆盖。走到最后说明read和dirty都没有,将值添加到dirty。

  • 获取值流程:

    先从read里面获取,获取到以后直接返回,没有获取到加锁再获取一次,如果还是没有获取到并且read.amended为true,说明此时dirty里面有read里面没有的值,从dirty里面获取,依然没获取到就直接返回false,获取到了则最后调用e.load(),返回有效值。注意有个missLocked,这个是判断read里面没有命中的次数,如果次数超过了dirty的长度,那么就将dirty赋值给read。这步操作才是真的解放map,因为read里面可能有些被标记为expunged的值,但是key还存在,此时dirty一覆盖,所有被删除的值才真正被删除。

    如果实际项目中map里面有大量的key在read里面,然后删除掉后,只是标记为删除,删掉的内存会下降吗?

  • delete流程:

    从read里面加载的值标记为nil,然后返回原来的值。如果read里面没有,则从dirty里面删,dirty里面不是标记删除,直接删,然后再执行下missLocked,看来删除和获取都会统计没有命中次数。

性能对比

参考golang sync.Map和map+mutex性能比较

可见sync.map 适用于读多写少场景,官方也有说明

Go\src\sync\map.go +12-26

// Map is like a Go map[interface{}]interface{} but is safe for concurrent use
// by multiple goroutines without additional locking or coordination.
// Loads, stores, and deletes run in amortized constant time.
//
// The Map type is specialized. Most code should use a plain Go map instead,
// with separate locking or coordination, for better type safety and to make it
// easier to maintain other invariants along with the map content.
//
// The Map type is optimized for two common use cases: (1) when the entry for a given
// key is only ever written once but read many times, as in caches that only grow,
// or (2) when multiple goroutines read, write, and overwrite entries for disjoint
// sets of keys. In these two cases, use of a Map may significantly reduce lock
// contention compared to a Go map paired with a separate Mutex or RWMutex.
//
// The zero Map is empty and ready for use. A Map must not be copied after first use.
  • 这种map 充分利用在下面两种场景,1种是仅仅只写一次,但是读很多次,另一种是当多个goroutines读写map,多次重写没有被删除的key,用sync.map 在这两种情况下可以有效的减少锁的使用 相较于互斥锁和读写锁。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值