线程安全的sync.Map解读以及使用

2 阅读7分钟

sync.Map

虽然 Go 1.24 把内置 map 的底层实现升级为基于 Swiss Table 的设计,优化了 map 操作速度,但是内置 map 仍然不是并发安全的。想要使用并发安全的 map 有几个方法:

  1. 使用 go 提供的 sync.Map 支持并发访问(比较适用于特殊场景)
  2. 在 GitHub 社区寻找成熟的并发 map 的实现,如:github.com/orcaman/con…
  3. 基于 RWMutex + map 自己封装一个并发安全的 map

接下来我们从 sync.Map 的源码去看是怎么实现的并发安全,在文章结尾我也会提供一个基于 RWMutex + map 的并发安全 map 的 demo。

使用场景

下面是 go 官方推荐的 sync.Map 的使用场景

// The Map type is optimized for two common use cases: (1) when the entry for a given // key is only ever written once but read many times, as in caches that only grow, // or (2) when multiple goroutines read, write, and overwrite entries for disjoint // sets of keys. In these two cases, use of a Map may significantly reduce lock // contention compared to a Go map paired with a separate [Mutex] or [RWMutex].

sync.Map 针对两类常见使用场景进行了优化:

  1. 某个给定 key 的条目只会被写入一次,但会被读取很多次,例如只增不减的缓存;
  2. 多个 goroutine 对彼此不重叠的 key 集合进行读取、写入和覆盖。

在这两种场景下,相比“普通 map + 单独的 Mutex 或 RWMutex”,使用 sync.Map 可以显著减少锁竞争。

数据结构

type Map struct {
	_ noCopy
	mu Mutex
	read atomic.Pointer[readOnly]
	dirty map[any]*entry
	misses int
}
  1. _ noCopy 用来阻止 sync.Map 在使用后被值拷贝。sync.Map 不是普通容器,而是带有并发协议的状态机,内部的锁、原子变量和多视图状态必须保持一致,一旦拷贝就会破坏这些不变量。
  2. read atomic.Pointer[readOnly] 只读 map,可以不加锁读
  3. dirty map[any]*entry 加锁处理的可读写 map ,dirty != nil 时 dirty 是完整视图,read 在 amended=true 时可能缺新 key,并且它们指向的是同一个 entry 对象。
  4. misses int 记录访问 read 的未命中次数,达到阈值则 dirty map 会被提升为新的 read map
  5. mu Mutex 互斥锁,控制并发访问 dirty 和 misses
type readOnly struct {
	m       map[any]*entry
	amended bool 
}

type entry struct {
	p atomic.Pointer[any]
}

readonly 包含两个参数: readOnly 只读 read 的 map,amended 标识 read map 中的 key-entry 对是否存在缺失 entry map 中的 value,封装一个 atomic.Pointer 指向 value 的指针,有三种状态:

  1. 指向实际的值
  2. 删除操作第一阶段会将 p 原子地设为 nil
  3. 在 Dirty map 重新构建或清理时,nil 状态会进一步转化为 expunged

Load 读数据

map.Load(key any)

func (m *Map) Load(key any) (value any, ok bool) {
	read := m.loadReadOnly()
	e, ok := read.m[key]
	if !ok && read.amended {
		m.mu.Lock()
		read = m.loadReadOnly()
		e, ok = read.m[key]
		if !ok && read.amended {
			e, ok = m.dirty[key]
			m.missLocked()
		}
		m.mu.Unlock()
	}
	if !ok {
		return nil, false
	}
	return e.load()
}

从 sync.Map 中读取 value 的方法:

  1. 首先调用 loadReadOnly()原子操作读取 m.read,如果没有则返回一个readOnly{}
  2. 判断 read 中是否有 key 对应的 entry,有的话直接返回
  3. 如果没有且 amended 为 false,直接返回 nil,false
  4. 否则对 m 加锁,再次查询 read, 还是没有且 amended 为 true 则需要去 dirty 查询,此时 read 的未命中调用 missLocked 对 misses++
  5. 解锁返回查询结果。

entry.load()

func (e *entry) load() (value any, ok bool) {
	p := e.p.Load()
	if p == nil || p == expunged {
		return nil, false
	}
	return *p, true
}

读操作中在前面取到 key 对应的 entry 后,需要对 entry 封装 p 进行原子加载

  1. p == nil || p == expunged 则代表,key 已经被删除返回 nil
  2. 否则读 p 指针内容,并且转为 any 的形式进行返回.

map.missLocked()

func (m *Map) missLocked() {
	m.misses++
	if m.misses < len(m.dirty) {
		return
	}
	m.read.Store(&readOnly{m: m.dirty})
	m.dirty = nil
	m.misses = 0
}

当需要 dirty 兜底时则会调用该方法,会对 misses 进行++,并且在需要时将 dirty 提升为 read,并将 dirty 置为 nil 和 misses 清零

Store 写数据

map.Store(key, value any)

func (m *Map) Store(key, value any) {
	_, _ = m.Swap(key, value)
}

做了对 Swap 的封装调用,主要看 Swap 的实现

map.Swap(key, value any)

func (m *Map) Swap(key, value any) (previous any, loaded bool) {
	read := m.loadReadOnly()
	if e, ok := read.m[key]; ok {
		if v, ok := e.trySwap(&value); ok {
			if v == nil {
				return nil, false
			}
			return *v, true
		}
	}
	m.mu.Lock()
	read = m.loadReadOnly()
	if e, ok := read.m[key]; ok {
		if e.unexpungeLocked() {
			m.dirty[key] = e
		}
		if v := e.swapLocked(&value); v != nil {
			loaded = true
			previous = *v
		}
	} else if e, ok := m.dirty[key]; ok {
		if v := e.swapLocked(&value); v != nil {
			loaded = true
			previous = *v
		}
	} else {
		if !read.amended {
			m.dirtyLocked()
			m.read.Store(&readOnly{m: read.m, amended: true})
		}
		m.dirty[key] = newEntry(value)
	}
	m.mu.Unlock()
	return previous, loaded
}
  1. 如果 read 中有对应的 key 存在,就进入 trySwap 判断,在 trySwap 函数里,会判断现在 entry.p 是否是 expunged ,如果不是则直接基于 CAS 操作进行 entry 值的更新,然后返回
  2. 若前面没有更新,则对 m 进行加锁,进行二次判断 read 中是否存在对应 key
  3. 存在的话,首先调用 unexpungeLocked 原子操作判断 entry.p 不是 expunged 返回 false 则直接原子更新 entry 为新值;如果 entry.p 是 expunged 则将 entry.p 置为 nil 返回 true 补齐 dirty 的 key-entry ,然后原子更新值
  4. 如果 read 中不存在对应的 key 且 dirty 中存在对应的 key 则直接原子更新 dirty 的 entry 为新值
  5. 如果 read 和 dirty 中都不存在 key-entry,1. read.amended 为 false 则调用 dirtyLocked() ,在 dirtyLocked()中如果 dirty 为 nil 则会创建一个 dirty 并将 read 中没被删除的 key-entry 循环赋值给 dirty,如果 dirty 不为 nil 直接返回,然后更新 read.amended 为 true;2. 如果 read.amended 为 true 则直接给 dirty 赋值
  6. 解锁返回

*entry.trySwap(i any)

func (e *entry) trySwap(i *any) (*any, bool) {
	for {
		p := e.p.Load()
		if p == expunged {
			return nil, false
		}
		if e.p.CompareAndSwap(p, i) {
			return p, true
		}
	}
}

写操作中根据传进来的 entry 执行不同操作 entry.p 为 expunged 此时 entry.p 已经被删除,直接返回 false,否则原子 cas 操作更新 entry.p 的值

entry.unexpungeLocked()

func (e *entry) unexpungeLocked() (wasExpunged bool) {
	return e.p.CompareAndSwap(expunged, nil)
}

写操作中如果 entry.p 为 expunged 则 cas 更新为 nil 并返回 true ,否则返回 false

*entry.swapLocked(i any)

func (e *entry) swapLocked(i *any) *any {
	return e.p.Swap(i)
}

写操作中如果 read map 或者 dirty map 存在对应 key-entry,最终会通过原子操作更新 entry.p 。

m.dirtyLocked()

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

	read := m.loadReadOnly()
	m.dirty = make(map[any]*entry, len(read.m))
	for k, e := range read.m {
		if !e.tryExpungeLocked() {
			m.dirty[k] = e
		}
	}
}

写操作中,如果需要对 dirty 插入数据且此时 amended 为 false,首先判断 dirty 是否为 nil

  1. 不为 nil,直接返回
  2. dirty 为 nil 未初始化,创建一个新的 dirty,然后遍历 read map,此时会判断 value 是否被删除,只有不被删除的才会讲 key-entry 加入到 dirty

entry.tryExpungeLocked()

func (e *entry) tryExpungeLocked() (isExpunged bool) {
	p := e.p.Load()
	for p == nil {
		if e.p.CompareAndSwap(nil, expunged) {
			return true
		}
		p = e.p.Load()
	}
	return p == expunged
}

在写流程中,如果需要将 read map 复制给 dirty 则执行该函数

  1. 如果 entry.p 为 nil 则原子操作改为 expunged
  2. 返回 p == expunged

Delete 删除数据

m.Delete(key any)

func (m *Map) Delete(key any) {
	m.LoadAndDelete(key)
}

封装了对 LoadAndDelete 调用,主要看 LoadAndDelete 的实现

m.LoadAndDelete(key any)

func (m *Map) LoadAndDelete(key any) (value any, loaded bool) {
	read := m.loadReadOnly()
	e, ok := read.m[key]
	if !ok && read.amended {
		m.mu.Lock()
		read = m.loadReadOnly()
		e, ok = read.m[key]
		if !ok && read.amended {
			e, ok = m.dirty[key]
			delete(m.dirty, key)
			m.missLocked()
		}
		m.mu.Unlock()
	}
	if ok {
		return e.delete()
	}
	return nil, false
}
  1. 首先读取 read map,如果 read map 存在对应的 key-entry 直接调用 e.delete 返回
  2. 如果 read map 不存在对应得 key-entry 且 amended 为 false,直接返回
  3. 如果 amended 为 true 则加锁,二次检查 read map,如果 read map 此时存在对应 key-entry 则解锁,调用 e.delete 返回
  4. read map 二次检查仍不存在对应 key 且 amended 为 true,尝试从 dirty map 删除对应得 key-entry,此时需要 dirty 兜底会执行 missLocked,解锁,根据 ok 的值决定后续操作

entry.delete()

func (e *entry) delete() (value any, ok bool) {
	for {
		p := e.p.Load()
		if p == nil || p == expunged {
			return nil, false
		}
		if e.p.CompareAndSwap(p, nil) {
			return *p, true
		}
	}
}

在删除流程中,如果 read map 或 dirty map 中存在对应的 key,则调用 delete(),根据 entry.p 得值执行不同操作

  1. p == nil || p == expunged,说明 entry 已经处于删除态,直接返回
  2. 否则将 p 通过 cas 操作赋值为 nil

遍历数据

m.Range(f func(key, value any) bool)

func (m *Map) Range(f func(key, value any) bool) {
	read := m.loadReadOnly()
	if read.amended {
		m.mu.Lock()
		read = m.loadReadOnly()
		if read.amended {
			read = readOnly{m: m.dirty}
			copyRead := read
			m.read.Store(&copyRead)
			m.dirty = nil
			m.misses = 0
		}
		m.mu.Unlock()
	}

	for k, e := range read.m {
		v, ok := e.load()
		if !ok {
			continue
		}
		if !f(k, v) {
			break
		}
	}
}

遍历流程,如果 amended 为 true,read map 缺数据则加锁将 dirty map 赋值给 read map,并清空 misses, 接着 for 循环遍历 read map 取值,如果 value 在执行用户传入的回调函数时为 false 则结束遍历

RWMutex + map 支持并发安全的 map demo

代码篇幅较长,附上我的仓库连接,麻烦点击连接查看: github.com/Wsp030914/R…

谢谢阅读!