众所周知关于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等等,就无需考虑