Present Day, Present Time

gobomb

Golang Sync Map

适用的情形是 1. 读多写少 2. 多个g读写的key不同。

使用冗余的 map,来分离读写,相当多了一个缓存。两个map分别为dirty、read,他们的value存的是指针,相当于value是共享的,节省一些内存。

type Map struct {
    // 保护dirty、misses,read的提升过程
	mu Mutex
    // 如果是read中的已知key,不需要加锁
	read atomic.Value 
    // 新加的key会写到dirty中
	dirty map[interface{}]*entry
    // 记录从read未命中,到dirty中去查的次数
	misses int
}

// read对应的数据结构
type readOnly struct {
	m       map[interface{}]*entry
	amended bool 
}

// 表示value的指针
type entry struct {
	p unsafe.Pointer 
}

对dirty的增删查改需要加锁,对read的更新、读、删除则不需要,使用原子操作实现。所以这里性能比一般的map+mutex更好的地方,在于原子操作比mutex快。

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()
		// ...
		m.mu.Unlock()
	}
	if !ok {
		return nil, false
	}
	return e.load()
}

func (m *Map) Store(key, value interface{}) {
	read, _ := m.read.Load().(readOnly)
	if e, ok := read.m[key]; ok && e.tryStore(&value) {
		return
	}

	m.mu.Lock()
	// ...
	m.mu.Unlock()
}

func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool) {
	read, _ := m.read.Load().(readOnly)
	e, ok := read.m[key]
	if !ok && read.amended {
		m.mu.Lock()
		// ...
		m.mu.Unlock()
	}
	if ok {
		return e.delete()
	}
	return nil, false
}

对key的更新、读、删除会先对read操作,当从read找不到key时,才加锁到dirty中查找。每次在read没有命中,misses就会加1。

当misses>=len(dirty)时,dirty就会提升为read。dirty本身会被置为nil。

func (m *Map) missLocked() {
	m.misses++
	if m.misses < len(m.dirty) {
		return
	}
    // 把dirty提升为read,dirty置nil,misses清0
    // readOnly的amended此时默认值是fasle
	m.read.Store(readOnly{m: m.dirty})
	m.dirty = nil
	m.misses = 0
}

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, _ = m.read.Load().(readOnly)
        // 加锁后会再检查一次read,防止读read之后加锁之前read发生变化
		e, ok = read.m[key]
		if !ok && read.amended {
            // read中找不到key,到dirty中查找
			e, ok = m.dirty[key]
            // 不管有没有找到,都会记录 misses
			m.missLocked()
		}
		m.mu.Unlock()
	}
	if !ok {
		return nil, false
	}
	return e.load()
}

添加新key(read中不存在的key),会加锁并添加到dirty中去。

func (m *Map) Store(key, value interface{}) {
	read, _ := m.read.Load().(readOnly)
	if e, ok := read.m[key]; ok && e.tryStore(&value) {
		return
	}

	m.mu.Lock()
    // 加锁后会再检查一次read,防止读read之后加锁之前read发生变化
	read, _ = m.read.Load().(readOnly)
	if e, ok := read.m[key]; ok {
		if e.unexpungeLocked() {
            // expunged意味着这里的key在read被删除过
            // dirty从read中复制key的时候,没有把被删除的key复制过来
			m.dirty[key] = e
		}
		e.storeLocked(&value)
	} else if e, ok := m.dirty[key]; ok {
		e.storeLocked(&value)
	} else {
        // amended为false,说明dirty需要分配空间
		if !read.amended {
			m.dirtyLocked()
            // amended修改为true
			m.read.Store(readOnly{m: read.m, amended: true})
		}
		m.dirty[key] = newEntry(value)
	}
	m.mu.Unlock()
}

read有个amended属性,为true则表示dirty包含read没有的key,为false则表示read刚刚提升过,dirty为nil。这时dirty就需要分配空间,并且把read中未删除的key复制到dirty中(dirtyLocked),然后将read.amended置为true。

func (m *Map) dirtyLocked() {
	if m.dirty != nil {
		return
	}

	read, _ := m.read.Load().(readOnly)
    // 给dirty分配内存
	m.dirty = make(map[interface{}]*entry, len(read.m))
    // 把read中未删除的key复制到dirty中
	for k, e := range read.m {
        // 删除过的key会把对应的value标记为expunged
		if !e.tryExpungeLocked() {
			m.dirty[k] = e
		}
	}
}

Ref

go1.17.6 go/src/sync/map.go