golang中map当前版本默认直接并发写会报concurrent map writes 错误
在golang中要实现并发读写的话有三种目前通用的方式:
1. 使用读写锁sync.RWMutex,在读的时候使用读锁,使用的时候如下代码,效率较低:
var counter = struct{
sync.RWMutex //读写锁
m map[string]int
}{m: make(map[string]int)}
counter.RLock() // 读锁定
n := counter.m["some_key"]
counter.RUnlock()
unter.Lock()// 写锁定
counter.m["some_key"]++
counter.Unlock()
2. 使用golang官方包的syncmap,效率比第一种方式要高。源码在原生包sync.Map,或者github:https://github.com/golang/sync/tree/master/syncmap
3. 使用其他开源包的concurrentmap,或者自己实现,可以参考java的concurrentmap,使用shard将数据分块,每个锁只针对一个块。
该文章主要讲syncmap的流程,并在源码中翻译原来注释加入一些中文注释帮助理解,配合源码食用效果更佳。
syncmap是由两个map构成,一个只读readmap,一个写dirtymap。两个map的存储的value都是entry一个指向具体值的指针,如果一个key在read和dirty中同时存在那么修改的时候通过read来原子修改即可,如果value为nil或者expunged代表被删除。
在代码里面通过原子操作+循环对比来实现lockfree。在操作read的时候使用的是lockfree的方式,操作dirty的时候需要加锁。
在每次lockfree进入lock之后,由于之前判断情况时候并没有上锁,所以是可能已经改变的,需要再取值判断一次情况。这种情况在整个源码从lockfree到lock时候大量出现。
读的时候优先往read中lockfree读,read中读不到的话再往dirytymap 中lock查找,记录未命中次数,当达到一定次数之后将dirtymap中的数据全部复制到read中。
读取情况如下如下:
read中没有的意思是指没有对应key,entry指向nil,entry指向expunged
在存储数据的时候只往dirtymap中写
存储情况如下:
删除的话采用滚动删除,流程如下:
从删除的流程中可以看出,当dirty没有生成时候,read中不含有expunged,被删除的数据全部是nil。dirty中不含有expunged的数据。read中会保留上一轮dirty所有的key,只是在复制到dirty的时候nil和expunged的不复制,从而实现滚动删除key
//Map是并发安全的,零Map是空且有效的
type Map struct {
mu sync.Mutex
//read包含Map的一部分内容,并且不需要持有锁即可以并发访问
//read本身load的时候是安全的,但是装载的时候需要持有锁
//Entries 存储在read中可能没有锁情况下被并发更新,但是更新先前删除的条目需要将条目复制到dirtymap并且用mu锁的情况下更新为未被擦除。
read atomic.Value // readOnly
//dirty 包含一部分需要持有锁访问的内容。Dirty map中同时也包含没有被擦去的read entries 来确保快速dirty->read
// 如果 dirtymap 是nil的话,下次写的时候从read浅复制来初始化,略过过时条目
dirty map[interface{}]*entry
//misses 统计从read最后一次更新后(dirty转read)需要锁才能知道key是否存在
//一旦misses到了一定数量之后就把dirty->read里,下次存数据的时候初始化一个复制出一个新的dirty
misses int
}
// readOnly是一个不可变的结构,以原子方式存储在Map.read字段中.
type readOnly struct {
m map[interface{}]*entry
amended bool // true if the dirty map contains some key not in m.
}
//expunged 是一个任意指针,用于标记从dirymap已删除的条目
var expunged = unsafe.Pointer(new(interface{}))
type entry struct {
// p points to the interface{} value stored for the entry.
//p 指向 value 存在 entry中的interface{}
// If p == nil, the entry has been deleted and m.dirty == nil.
//如果p==nil,entry已经被删除,并且m.dirty==nil(最近一次dirty->read后没有插入操作,尚未未初始化)
// If p == expunged, the entry has been deleted, m.dirty != nil, and the entry is missing from m.dirty.
// 如果p==expunged,entry已经被删除,m.dirty!=nil(已经初始化),并且m.dirty中没有这个entry(已经删除)
// Otherwise, the entry is valid and recorded in m.read.m[key] and, if m.dirty!= nil, in m.dirty[key].
//否则的话,entry 有效并且记录在 read[key]中,如果dirty不为空的话,也在dirty[key]中
// An entry can be deleted by atomic replacement with nil: when m.dirty is next created, it will atomically replace nil with expunged and leave m.dirty[key] unset.
//entry 可以被删除通过原子替换为nil,当dirty被创建后,原子替换为expunged 并且在dirty中置空
// An entry's associated value can be updated by atomic replacement, provided p != expunged. If p == expunged, an entry's associated value can be updated
// only after first setting m.dirty[key] = e so that lookups using the dirty map find the entry.
// 如果p!=expunged 可以通过原子替换更新entry关联值,p==expunged,先设置dirty[key] = e 之后再更新 entry的关联值
p unsafe.Pointer // *interface{}
}
// Store sets the value for a key.
func (m *Map) Store(key, value interface{}) {
//原子获取read
read, _ := m.read.Load().(readOnly)
//如果read中有,并且没有被擦除的话,直接更新指针
//read中没有,需要插入
//被擦除说明dirty已经初始化,并且部在dirty中,不能直接更新,需要dirty插入
if e, ok := read.m[key]; ok && e.tryStore(&value) {
return
}
//上锁流程
m.mu.Lock()
//进入lock需要把lockfree中的情况再次判断
read, _ = m.read.Load().(readOnly)
if e, ok := read.m[key]; ok {
//存在,说明被擦除了,先expunged变为unexpunged(nil或者其他值),这时候已经可以并发更新了,返回失败的话说明这个时候已经被先前并发程序置为unexpunged了,同时意味着dirty中加入了entry
if e.unexpungeLocked() {
//把dirty中加入entry
m.dirty[key] = e
}
//entry赋值
e.storeLocked(&value)
} else if e, ok := m.dirty[key]; ok {
//read中没有,dirty中有的话直接替换entry值即可
e.storeLocked(&value)
} else {
//如果read的修订标志是false的话,需要初始化dirtymap
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.
//初始化dirtymap
m.dirtyLocked()
//使用原子替换
m.read.Store(readOnly{m: read.m, amended: true})
}
//值插入
m.dirty[key] = newEntry(value)
}
m.mu.Unlock()
}
// tryStore stores a value if the entry has not been expunged.
// If the entry is expunged, tryStore returns false and leaves the entry unchanged.
// 尝试存储value,如果entry还没被擦除的话。如果已经被擦除的话返回false,值不变。被擦除的话再,插入需要插入到dirty,否则的话直接更新e.p即可,dirty持有的是指针
func (e *entry) tryStore(i *interface{}) bool {
p := atomic.LoadPointer(&e.p)
if p == expunged {
return false
}
for {
if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
return true
}
p = atomic.LoadPointer(&e.p)
if p == expunged {
return false
}
}
}
// unexpungeLocked 确保entry不是expunged,在mu解锁之前要保证entry被加入到dirty中,返回false的话说明已经不是expunged(已经初始化dirtymap)
func (e *entry) unexpungeLocked() (wasExpunged bool) {
return atomic.CompareAndSwapPointer(&e.p, expunged, nil)
}
// storeLocked unconditionally stores a value to the entry.
// The entry must be known not to be expunged.
//
func (e *entry) storeLocked(i *interface{}) {
atomic.StorePointer(&e.p, unsafe.Pointer(i))
}
//初始化dirtymap
func (m *Map) dirtyLocked() {
if m.dirty != nil {
return
}
read, _ := m.read.Load().(readOnly)
m.dirty = make(map[interface{}]*entry, len(read.m))
for k, e := range read.m {
//已经为expunged,为nil的话换为expunged,这两种情况不复制进dirty
if !e.tryExpungeLocked() {
m.dirty[k] = e
}
}
}
//如果为expunged或者nil返回true,并且把nil换为expunged
func (e *entry) tryExpungeLocked() (isExpunged bool) {
p := atomic.LoadPointer(&e.p)
for p == nil {
if atomic.CompareAndSwapPointer(&e.p, nil, expunged) {
return true
}
p = atomic.LoadPointer(&e.p)
}
return p == expunged
}
// Load returns the value stored in the map for a key, or nil if no
// value is present.
// The ok result indicates whether value was found in the map.
func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
//原子加载只读
read, _ := m.read.Load().(readOnly)
e, ok := read.m[key]
//如果read中没有并且dirtymap已经被初始化的话,从dirtymap中找
if !ok && read.amended {
m.mu.Lock()
// Avoid reporting a spurious miss if m.dirty got promoted while we were
// blocked on m.mu. (If further loads of the same key will not miss, it's
// not worth copying the dirty map for this key.)
read, _ = m.read.Load().(readOnly)
e, ok = read.m[key]
//重新判断,因为在锁前read可能已经经历过一轮变化
if !ok && read.amended {
e, ok = m.dirty[key]
//无论条目是否存在,记录一次未命中,miss次数和>=len(dirty)的话,将dirty提升为read
m.missLocked()
}
m.mu.Unlock()
}
if !ok {
return nil, false
}
return e.load()
}
//无论条目是否存在,记录一次未命中,miss次数和>=len(dirty)的话,将dirty提升为read
func (m *Map) missLocked() {
m.misses++
if m.misses < len(m.dirty) {
return
}
m.read.Store(readOnly{m: m.dirty})
m.dirty = nil
m.misses = 0
}
// Delete deletes the value for a key.
func (m *Map) Delete(key interface{}) {
read, _ := m.read.Load().(readOnly)
e, ok := read.m[key]
//如果不存在,并且dirty已经加载的话删除dirty中的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()
}
}
func (e *entry) delete() (hadValue bool) {
for {
p := atomic.LoadPointer(&e.p)
if p == nil || p == expunged {
return false
}
if atomic.CompareAndSwapPointer(&e.p, p, nil) {
return true
}
}
}
LoadOrStore:如果有数据的话返回,没有的话存储,总体思路和上面差不多
Range:遍历,如果dirty有的话加锁便利dirty,如果dirty没有的话遍历read
如果有没有表达清楚或者有误的地方欢迎斧正,码字不易,要是对您有所帮助的话还请点个赞