go源码之sync.Map

go源码之sync.Map

go 源码之 sync.Map

参考资料

Golang sync.Map 原理(两个 map 实现 读写分离、适用读多写少场景) (dgrt.cn)

总结

  • sync.Map 是并发安全的,内部采用读(read)写(dirty)分离,常用于多读少写场景

  • 不适用存储大量数据,因为最坏情况下,sync.Map 的内存会翻倍

  • 设计理念

    • 读写分离

      read:只读结构,采用原子操作,无须加锁,提高性能

      dirty:普通 map,读写都加锁

      misses:从 dirty 中读取的次数,misses>=Len(dirty) dirty 升级为 read (直接复制)

    • 延迟删除

      删除只是为 value 打一个标记,在 dirty map 提升时才执行真正的删除

    • 降低锁的粒度

      使用只读结构 read,原子操作,降低锁的粒度

源码

结构


// 安全读写 map
type Map struct {
	mu    Mutex          // 当写read map 或读写dirty map时 需要上锁
	read  atomic.Value   // readOnly 只读结构,,原子操作,包含了map中的一部分key:value,另外在dirty字段中
	dirty map[any]*entry // dirty 字段包括读写都需要加锁的字段,会升级为read字段

	misses int // 每次从 dirty 查找时 misses+1 当 misses 达到diry map len时,dirty被提升为read 并且重新分配dirty
}


type readOnly struct { // read 结构
	m       map[any]*entry // 存储实际元素
	amended bool           // true:表 dirty 有 read 无,可快速判断 dirty 是否存在元素
}
type entry struct {
	p unsafe.Pointer // 指向元素的地址  1. nil:正常;2. expunged:被删除;3. 指向实际元素地址
}

读 Load

  • read 中存在:直接返回

  • read 中不存在:

    • amended =false,则返回 false

    • amended = true,表示 dirty 中存在 read 没有的数据,加锁,从 dirty 中读取;

      misses++,当 misses== len(dirty):将 dirty 数据赋值给 read ,并且 read 内的 amended=false,清空 dirty

func (m *Map) Load(key any) (value any, ok bool) {
  if read  中存在:直接返回 {
    return value,ok
  }
  
  if read  中不存在 {
    if read.amended ==false{
      return nil,false
    }
    上锁
     dirty 读取  valueok 
    misses++
    if  misses== len(dirty) {
      read=dirty
      amended=false
      dirty=nil
    }
    解锁
    return value,ok
  }
}
// 读取
func (m *Map) Load(key any) (value any, ok bool) {
	// 原子操作,先从read结构中读取
	read, _ := m.read.Load().(readOnly)
	// 读取元素
	e, ok := read.m[key]
	// 元素在read结构中不存在,但是 amended=true,表示在dirty中存在
	if !ok && read.amended {
		// 存在dirty,加锁读取
		m.mu.Lock()
		// double check,避免在加锁的时候dirty map提升为read map
		read, _ = m.read.Load().(readOnly)
		e, ok = read.m[key]
		if !ok && read.amended { // read 不存在,但是amended=true表示在 dirty 中存在
			e, ok = m.dirty[key] // 从dirty中读取
			m.missLocked()       // misses+1 && 判断是否需要提升dirty 为 read
		}
		m.mu.Unlock()
	}
	if !ok {
		return nil, false
	}
	// 返回结果
	return e.load()
}

dirty 升级为 read

func (m *Map) missLocked() {
	m.misses++

	if m.misses < len(m.dirty) {
		return
	}
	// 如果misses 大于等于 dirty的长度;则提升 dirty 为read
	m.read.Store(readOnly{m: m.dirty})
	m.dirty = nil
	m.misses = 0
}

写 Store

  • read 中存在 key,并且不是删除状态,则直接原子操作更新
  • read 中不存在 key 或 该 key 不可直接更新:
    • 上锁

    • read 中 存在 key:则同时更新 read 和 dirty

    • read 中 不存在 key,但是存在 dirty:则更新 dirty

    • 全新的 key,不存在 read 和 dirty:

      • 首先判断如果 read.amended == false,则改为 true,并且判断 dirty==nil 时,将 read 加载到 dirty
      • 最后 更新 dirty
    • 解锁

// Store sets the value for a key.
func (m *Map) Store(key, value any) {
	// 如果read map中存在该key 且不是删除状态  则尝试直接更改(由于修改的是entry内部的pointer,因此dirty map也可见)
	read, _ := m.read.Load().(readOnly)
	if e, ok := read.m[key]; ok && e.tryStore(&value) {
		return
	}

	// 无法在`read`中更新,则更新在 dirty
	m.mu.Lock()
	read, _ = m.read.Load().(readOnly)
	if e, ok := read.m[key]; ok { // 如果 key 在 read 中存在
		if e.unexpungeLocked() { // 但 p == expunged,则说明 key 被删除,则 将p的状态由 expunged  更改为 nil
			m.dirty[key] = e // b. dirty map 插入 key
		}
		e.storeLocked(&value) // 将 value 存储
	} else if e, ok := m.dirty[key]; ok {
		// 如果 key 在 read 不存在,但是 dirty 存在
		e.storeLocked(&value) // 则更新value
	} else {
		// 如果 key 在 read 和 dirty 都不存在
		if !read.amended {
			m.dirtyLocked()                                  // 将 read 中所有的非删除的元素 加载 到 dirty
			m.read.Store(readOnly{m: read.m, amended: true}) // 将amended=true,表示 read 中没有,但是 dirty 中有
		}
		// 存入 dirty
		m.dirty[key] = newEntry(value)
	}
	m.mu.Unlock()
}

m.dirtyLocked()

是一个整体的指针交换操作。
当之前执行 Load() 方法且满足条件 misses=len(dirty) 时,会将 dirty 数据整体迁移到 read 中。

sync.Map 直接将原 dirty 指针 store 给 read 并将 dirty 自身也置为 nil。

因此 sync.Map 若想要保证在 amended=true(read 和 dirty 中数据不一致),并且下一次发生数据迁移时(dirty → read)不会丢失数据,dirty 中就必须拥有整个 Map 的全量数据才行。所以这里 m.dirtyLocked() 又会【将 read 中未删除的数据加入到 dirty 中】。
不过 dirtyLocked() 是通过一个迭代实现的元素从 read 到 dirty 的复制,如果 Map 中元素数量很大,这个过程付出的损耗将很大,并且这个过程是在保护下的。这里迭代遍历复制的方式可能会存在性能问题

// 将 read 中所有未删除的元素复制到 dirty
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
		}
	}
}

删 Delete

  • 存在 read 中,则直接将元素置为 nil
  • 不存在 read 但是存在 dirty 中,则直接 delete 删除
func (m *Map) Delete(key any) {
	m.LoadAndDelete(key)
}
func (m *Map) LoadAndDelete(key any) (value any, loaded bool) {
	read, _ := m.read.Load().(readOnly)
	e, ok := read.m[key]
	if !ok && read.amended {
		// 存在 dirty 中
		m.mu.Lock()
		read, _ = m.read.Load().(readOnly)
		e, ok = read.m[key]
		if !ok && read.amended {
			// 从 dirty 中读取
			e, ok = m.dirty[key]
			delete(m.dirty, key) // 删除

			m.missLocked() // misses++
		}
		m.mu.Unlock()
	}
	if ok { // read 中存在,则直接将元素p置为nil
		return e.delete()
	}
	return nil, false
}

常见问题

1. sync.Map 结构用了原子操作 read atomic.Value,那为什么不直接使用原子操作即可,还要加一个 dirty

​ 原子操作可以提高读的效率,无法提高写的效率,相反写的效率还不如锁,这也就是为什么 sync.Map 推荐多读少写的原因;

假设 sync.Map 只用 read 实现,那读操作可以提高效率,但是写的性能将会很差,而且使用 dirty + 锁,可以提高 read 并发写的效率

总而言之,原子操作读的效率较高,写的效率还不如加锁,加锁只是应用层面上的控制,原子操作上独占 CPU 操作,简单的操作会比较快,如果写操作,需要进行额外的同步和协调操作

2. sync.Map 的使用场景

多读少写;原因:

  • 写 dirty 需要加锁
  • 写 read 是原子操作,性能差
  • 全新的 key 时,如果 dirty =nil 时,还会将 read 中所有数据加载到 dirty ,这个过程是加锁的,耗时

3. 为什么 sync.Map 不能被复制

​ 内置了一个 sync.Mutex,不可复制

4. sync.Map 是如何从 dirty 升级为 read 的

​ 每次读取 dirty 时,字段 misses++,直到 misses >= Len(dirty) 时,dirty 的数据赋值给 read,然后清空 dirty

5. sync.Map 的设计理念

​ 读写分离、空间换取时间、降低锁的粒度、

6. sync.Map 中的 readOnly 中的 amended 的设计?

amended 表示 read 和 dirty 元素不一致,也就是 dirty 中存在 read 没有的元素

7. 什么情况下元素会同时存在 read 和 dirty,并且为什么?

​ 当 dirty==nil 时,且插入一个全新的 key 时,read 的数据会全部加载到 dirty;

数据重复是因为,当 dirty 升级为 read 时,如果 dirty 没有 read 的数据,那么 数据就会丢失

8. sync.Map 最坏情况下内存翻倍

为了能将 dirty 中的数据快速提升为 readonly,dirty 中包含了 readonly 中的全部数据,意味着最坏情况下内存占用翻倍,

如果存储了大量数据的话,需要关注这点