最近做项目,涉及到对一个map做并发读写,为了避免并发问题,每次操作的时候会添加读写锁。下午有一点时间,想着找下看golang有没有并发安全的map,于是就找到了sync.Map,那么问题来了,自己手工加读写锁跟使用sync.Map除了多些代码之外,有没有其他性能上的区别呢?为了尽量提高并发读写,一个考虑是对key做分段加锁,这样可以同时对多个key做操作。但是具体怎么实现呢?学习下
Map包含的元素
type Map struct {
//一个互斥锁
mu Mutex
//一个原子类(cas更新),存储可以不需要锁就可以做并发读取的元素
read atomic.Value // readOnly
//dirty包含的元素需要使用排它锁进行访问
dirty map[interface{}]*entry
//从readMap获取不到元素的次数,根据这个读缺失的次数来判断是否要把dirty元素写入到readMap
misses int
}
从Map的定义可以看到,golang的并发安全的Map实现原理是:把已经写入并且未做修改的部分和做了修改的部分拆开,没有做修改的部分可以被多个现成读取,而一旦做了插入或者更新,这部分数据会存到dirty中,对dirty的读写操作需要使用排它锁
func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
read, _ := m.read.Load().(readOnly)
e, ok := read.m[key]
if !ok && read.amended {
m.mu.Lock()
//再读取下read,以免在这期间,dirty中的数据转移到了dirty
read, _ = m.read.Load().(readOnly)
e, ok = read.m[key]
if !ok && read.amended {
e, ok = m.dirty[key]
//这里不管是否在dirty中找到指定的元素,都会对miss+1
m.missLocked()
}
m.mu.Unlock()
}
if !ok {
return nil, false
}
return e.load()
}
看下写操作
func (m *Map) Store(key, value interface{}) {
//由于read的元素使用了cas更新,所以对于read中包含的entryKey,可以直接做更新,这样的更新操作会更加轻量(对于已经删除的元素,read中的entry会被置为expunged表示已删除)
read, _ := m.read.Load().(readOnly)
if e, ok := read.m[key]; ok && e.tryStore(&value) {
return
}
//若直接更新失败,则需要添加排它锁
m.mu.Lock()
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)
} else if e, ok := m.dirty[key]; ok {
e.storeLocked(&value)
} 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)
}
m.mu.Unlock()
}
func (m *Map) Delete(key interface{}) {
read, _ := m.read.Load().(readOnly)
e, ok := read.m[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)//删除dirty中的元素
}
m.mu.Unlock()
}
if ok {
//把entry置为nil
e.delete()
}
}
从实现来看,sync.Map适合读多写少的情况,更适合更新操作。由于每次插入的时候都需要添加排它锁,所以sync.Map不太适用于需要频繁插入的应用场景,这种情况自己添加读写锁来实现。