基本的数据结构
type Map struct {
mu sync.Mutex
read atomic.Value // readOnly
dirty map[interface{}]*entry
misses int
}
先来看看 dirty,是一个map类型的数据,键的类型是interface{},而值的类型是*entry,其结构如下
type entry struct {
p unsafe.Pointer // *interface{}
}
然后来看看read的数据结构,相较于dirty是多了一个amended属性的,用来表示是否存在key不存在于readOnly,而存在于dirty。
type readOnly struct {
m map[interface{}]*entry
amended bool // true if the dirty map contains some key not in m.
}
优势
func (m *Map) Store(key, value interface{}) {
read, _ := m.read.Load().(readOnly)
// 第一步,从 read 中读取,如果存在,则尝试直接修改
if e, ok := read.m[key]; ok && e.tryStore(&value) {
return
}
// 不存在,则需要使用锁
m.mu.Lock()
// 这个时候,需要再次判断 read 中是否存在
// 因为在锁获取到之前可能有同样的 key 插入到了 read 之中
read, _ = m.read.Load().(readOnly)
// 如果 read 中存在,则在 read 中修改
if e, ok := read.m[key]; ok {
if e.unexpungeLocked() {
m.dirty[key] = e
}
e.storeLocked(&value)
// 如果 dirty 中存在,则在 dirty 中修改
} else if e, ok := m.dirty[key]; ok {
e.storeLocked(&value)
// 都不存在,则需要在 dirty 中插入
} else {
if !read.amended {
m.dirtyLocked()
m.read.Store(readOnly{m: read.m, amended: true})
}
m.dirty[key] = newEntry(value)
}
m.mu.Unlock()
}
map中的并发读写容易出现问题,是因为在存值、删除、取值的过程中存在扩容的过程。而如果不对map进行修改呢?是不是就可以提高其执行效率了,因为这样就不存在扩容的过程了!!!
正是因为这样,readOnly和dirty中使用的map类型是map[interface{}]*entry,值是一个指针,通过对指针的操作来改变值的状态(比如,存储的值,是否被删除),而不是直接的删除或者赋值给map。
比如存值的第一步,如果key在read种存在,那么直接通过tryStore方法来进行修改值,如下:
func (e *entry) tryStore(i *interface{}) bool {
for {
p := atomic.LoadPointer(&e.p)
if p == expunged {
return false
}
if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
return true
}
}
}
这是使用了CAS,乐观锁。相当于对map中每个已经存在的值的修改使用乐观锁,相对于对整个map使用互斥锁或者读写锁来说,是极大的提高了效率的。
劣势
存值过程中可以看到,如果值并不存在于read之中,也是需要使用互斥锁的,而如果完全是新添加的key,还可能进行dirtyLocked的操作
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 {
if !e.tryExpungeLocked() {
m.dirty[k] = e
}
}
}
这个时候,需要完成从read到dirty的复制,这个对性能是极大的损耗的。
这个地方有一个点是非常值得注意的,由于
map[interface{}]*entry中的值是一个指针,所以在复制完成之后,对read里面的值进行修改,那么dirty中的值也是相应的会修改的,因为两者指向的是同一个对象。
总结
总的来说,sync.Map对于频繁修改的map效率是极高的,但是对于频繁增删的map,其效率是还不如使用sync.RWMutex的。
附录
- 关于
expunged的,这块单独说明下。通过源码可以看到,赋值entry.p = expunged的场景如下
var expunged = unsafe.Pointer(new(interface{}))
// 给sync.Map添加不存在于readOnly的元素,并且dirty=nil的时候。让dirty从readOnly中复制使用中的元素
func (m *Map) dirtyLocked() {
if m.dirty != nil {
return
}
read, _ := m.read.Load().(readOnly)
m.dirty = make(map[any]*entry, len(read.m))
for k, e := range read.m {
if !e.tryExpungeLocked() {
m.dirty[k] = e
}
}
}
// 如果值为nil,说明已经被delete了。这个时候设置成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
}
在完成 read到 dirty复制的过程中,如果 read中 map的 value是 nil,这个时候就给置为 expunged,以此表示从readOnly中(sync.Map)删除了。