golang sync.Map 源码解析

1,750 阅读3分钟

基本的数据结构

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进行修改呢?是不是就可以提高其执行效率了,因为这样就不存在扩容的过程了!!!

正是因为这样,readOnlydirty中使用的map类型是map[interface{}]*entry,值是一个指针,通过对指针的操作来改变值的状态(比如,存储的值,是否被删除),而不是直接的删除或者赋值给map

比如存值的第一步,如果keyread种存在,那么直接通过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
		}
	}
}

这个时候,需要完成从readdirty的复制,这个对性能是极大的损耗的。

这个地方有一个点是非常值得注意的,由于map[interface{}]*entry中的值是一个指针,所以在复制完成之后,对read里面的值进行修改,那么dirty中的值也是相应的会修改的,因为两者指向的是同一个对象。

总结

总的来说,sync.Map对于频繁修改的map效率是极高的,但是对于频繁增删的map,其效率是还不如使用sync.RWMutex的。

附录

  1. 关于 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
}
​

在完成 readdirty复制的过程中,如果 readmapvaluenil,这个时候就给置为 expunged,以此表示从readOnly中(sync.Map)删除了。