Map嵌套+Mutex,Go高并发操作Map

  众所周知关于Go的Map引用类型在多协程并发使用的时候不是协程安全的,使用Map进行并发修改时,如果低并发可能恰巧卡时间侥幸躲过。但高并发就没那么侥幸了:fatal error: concurrent map read and map write

  为什么不使用sync.Map

  因此大部分人可能会寻求使用sync.Map来保证协程安全,读写不冲突。先照搬一下sync.Map的一般的使用和适用场景:

  • 无须初始化,直接声明即可。普通的Map是需要用make初始化的,sync.Map:
    • var scene sync.Map
  • sync.Map 不能使用 map 的方式进行取值和设置等操作,而是使用 sync.Map 的方法进行调用,Store 表示新增和修改,Load 表示获取,Delete 表示删除
    • // 存键值,也可以做修改操作
      scene.Store("london", 100)
      // 根据键取值
      fmt.Println(scene.Load("london"))
      // 根据键删除键值对
      scene.Delete("london")
    • 而这三个函数的实现其实是通过锁机制完成的,因此实现了多线程并发情况下的互斥操作Map  
  • sync.Map其设计的初衷也只是解决大并发读写的问题,且非常适合于大量读少量写的情况,但是当sync.Map存在大量新增和删除时,会导致 dirty map 频繁更新,甚至在 miss 过多导致 dirty 成为 nil,并进入重建。其实sync.Map是由一下两个Map来实现的:(各Map的具体作用与Load,Store等方法的源码可参考 此处   
    • type Map struct {
          mu sync.Mutex
          read atomic.Value // readOnly
          dirty map[interface{}]*entry // 也就是说dirty map是由锁来实现load和store的
          misses int
      }
      
      type readOnly struct {
          m       map[interface{}]*entry
          amended bool // true if the dirty map contains some key not in m.
      }
      
      // entry则是map中的value
      type entry struct {
          p unsafe.Pointer // *interface{}
      }
  • 网上关于sync.Map与Map的性能比较:( 原文链接

sync.Map的性能高体现在读操作远多于写操作的时候。 极端情况下,只有读操作时,是普通map的性能的44.3倍。

反过来,如果是全写,没有读,那么sync.Map还不如加普通map+mutex锁呢。只有普通map性能的一半。

建议使用sync.Map时一定要考虑读定比例。当写操作只占总操作的<=1/10的时候,使用sync.Map性能会明显高很多。

  Map嵌套+Mutex实现方法

import "sync"

type Map struct {
    m map[string]map[string]string // 自定义K-V的类型
    sync.RWMutex
}

// 使用时初始化一个Map
var theMap = &Map{ m: make(map[string]map[string]string) }

// 读出相应Key的子Map
func (m *Map) LoadChildMap(key string) (value map[string]string, ok bool) {
    m.RLock()
    defer m.RUnlock()
    value, ok = m.m[key]
    return
}

// 读出相应Key的子Map里相应childKey的val
func (m *Map) LoadChildMapVal(key string, childKey string) (value string, ok bool) {
    m.RLock()
    defer m.RUnlock()
    valueMap, isOk := m.m[key]
    if !isOk {
        value, ok = "", false
        return
    }
    value, ok = valueMap[childKey]
    return
}

// 增加或修改相应Key的子Map
func (m *Map) StoreChildMap(key string, value map[string]string) {
    m.Lock()
    defer m.Unlock()
    m.m[key] = value
}

// 增加或修改相应Key的子Map里相应childKey的val
func (m *Map) StoreChildMapKV(key string, childKey string, value string) {
    m.Lock()
    defer m.Unlock()
    childMap, ok := m.m[key]
    if !ok {
        var newMap = make(map[string]string)
        newMap[childKey] = value
        m.m[key] = newMap
        return
    }
    childMap[childKey] = value
}

// 删除相应Key的子Map
func (m *Map) DeleteChildMap(key string) {
    m.Lock()
    defer m.Unlock()
    delete(m.m, key)
}

// 删除相应Key的子Map里的相应childKey的val
func (m *Map) DeleteChildMapVal(key string, childKey string) {
    m.Lock()
    defer m.Unlock()
    childMap, ok := m.m[key]
    if !ok {
        return
    }
    delete(childMap, childKey)
}
  • 需要注意的是,以上所有方法都是从头到尾加锁,若觉得粒度太大可以自己定义(甚至可以Map+Mutex整个结构嵌套,给每个子Map都设一把单独的Mutex锁,实现更小的粒度)。
  • 在自行定义方法的时候,若仅仅在   value, ok = m.m[key]  的前后加锁解锁从而达到减小粒度,但请注意不要在解锁后对value的任何值执行修改操作,因为Map的嵌套,value的类型是Map类型,是引用类型,对value进行操作实际上是对其指针进行操作,等效于绕过了锁机制直接进行修改,极易发生 fatal error: concurrent map writes
    • 解决办法
      • 将子Map值拷贝(需要深拷贝),待改好之后若需要更新到Map中,就再请求一个锁
      • 仍然遵守锁机制,待所有修改完后才解锁
    • 当如果Map的value不是Map,Slice这类引用类型时,而是值传递的int,string等等,就无需考虑
posted @ 2021-03-25 15:59  FakeStone  阅读(1352)  评论(0编辑  收藏  举报