并发安全map —— sync.Map

go原生的map不是线程安全的,在并发读写的时候会触发concurrent map read and map write的panic。 map应该是属于go语言非常高频使用的数据结构。早期go标准库并没有提供线程安全的map,开发者只能自己实现,后面go吸取社区需求提供了线程安全的map——sync.Map

sync.Map 提供5个如下api:

func (m *Map) Delete(key interface{}) //删除这个keyvalue func (m *Map) Load(key interface{}) (value interface{}, ok bool) //加载这个keyvalue func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool) //原子操作加载,如果没有则存储 func (m *Map) Range(f func(key, value interface{}) bool) //遍历kv func (m *Map) Store(key, value interface{}) //存储

使用sync.Map

package main import ( "fmt" "sync" ) func main() { sMap := sync.Map{} sMap.Store("a","b") ret,_:= sMap.Load("a") fmt.Printf("ret1 %t \n",ret.(string) == "b" ) ret,loaded :=sMap.LoadOrStore("a","c") fmt.Printf("ret2 %t loaded:%t \n",ret.(string) == "b",loaded ) ret,loaded =sMap.LoadOrStore("d","c") fmt.Printf("loaded %t \n",loaded) sMap.Store("e","f") sMap.Delete("e") sMap.Range(func(key, value interface{}) bool { fmt.Printf("k:%s v:%s \n", key.(string),value.(string)) return true }) }
$ go run main.go ret1 true ret2 true loaded:true loaded false k:a v:b k:d v:c

sync.Map底层实现

sync.Map的结构体

type Map struct { mu Mutex //互斥锁保护dirty read atomic.Value //存读的数据,只读并发安全,存储的数据类型为readOnly dirty map[interface{}]*entry //包含最新写入的数据,等misses到阈值会上升为read misses int //计数器,当读read的时候miss了就加+ }
type readOnly struct { m map[interface{}]*entry amended bool //dirty的数据和这里的m中的数据有差异的时候true }

从结构体上看Map有一定指针数据冗余,但是因为是指针数据,所以冗余的数据量不大。

Load的源码:

func (m *Map) Load(key interface{}) (value interface{}, ok bool) { read, _ := m.read.Load().(readOnly) //读只读的数据 e, ok := read.m[key] if !ok && read.amended { //如果没有读到且read的数据和drity数据不一致的时候 m.mu.Lock() read, _ = m.read.Load().(readOnly) //加锁后二次确认 e, ok = read.m[key] if !ok && read.amended { //如果没有读到且 read的数据和drity数据不一致的时候 e, ok = m.dirty[key] m.missLocked() //misses +1,如果 misses 大于等于 m.dirty 则发送 read的值指向ditry } m.mu.Unlock() } if !ok { return nil, false } return e.load() }

Store的源码:

func (m *Map) Store(key, value interface{}) { read, _ := m.read.Load().(readOnly) if e, ok := read.m[key]; ok && e.tryStore(&value) { //如果在read中找到则尝试更新,tryStore中判断key是否已经被标识删除,如果已经被上传则更新不成功 return } m.mu.Lock() read, _ = m.read.Load().(readOnly) //同上二次确认 if e, ok := read.m[key]; ok { if e.unexpungeLocked() {// 如果entry被标记expunge,则表明dirty没有key,可添加入dirty,并更新entry。 m.dirty[key] = e } e.storeLocked(&value) } else if e, ok := m.dirty[key]; ok { //如果dirty存在该key e.storeLocked(&value) } else { //key不存在 if !read.amended { //read 和 dirty 一致 // 将read中未删除的数据加入到dirty中 m.dirtyLocked() // amended标记为read与dirty不一致,因为即将加入新数据。 m.read.Store(readOnly{m: read.m, amended: true}) } m.dirty[key] = newEntry(value) } m.mu.Unlock() }

Delete的源码:

// Delete deletes the value for a key. func (m *Map) Delete(key interface{}) { read, _ := m.read.Load().(readOnly) e, ok := read.m[key] if !ok && read.amended { //在read中没有找到且 read和dirty不一致 m.mu.Lock() read, _ = m.read.Load().(readOnly) //加锁二次确认 e, ok = read.m[key] if !ok && read.amended { delete(m.dirty, key) //从dirty中删除 } m.mu.Unlock() } if ok { //如果key在read中存在 e.delete() //将指针置为nil,标记删除 } }

优缺点

优点: 通过read和dirty冗余的方式实现读写分离,减少锁频率来提高性能。 缺点:大量写的时候会导致read读不到数据而进一步加锁读取dirty,同时多次miss的情况下dirty也会频繁升级为read影响性能。 因此sync.Map的使用场景应该是读多,写少。

总结

本小节介绍了sync.Map的使用,通过源码的方式了解sync.Map的底层实现,同时介绍了它的优缺点,以及使用场景。