sync.Map
虽然 Go 1.24 把内置 map 的底层实现升级为基于 Swiss Table 的设计,优化了 map 操作速度,但是内置 map 仍然不是并发安全的。想要使用并发安全的 map 有几个方法:
- 使用 go 提供的 sync.Map 支持并发访问(比较适用于特殊场景)
- 在 GitHub 社区寻找成熟的并发 map 的实现,如:github.com/orcaman/con…
- 基于 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 针对两类常见使用场景进行了优化:
- 某个给定 key 的条目只会被写入一次,但会被读取很多次,例如只增不减的缓存;
- 多个 goroutine 对彼此不重叠的 key 集合进行读取、写入和覆盖。
在这两种场景下,相比“普通 map + 单独的 Mutex 或 RWMutex”,使用 sync.Map 可以显著减少锁竞争。
数据结构
type Map struct {
_ noCopy
mu Mutex
read atomic.Pointer[readOnly]
dirty map[any]*entry
misses int
}
- _ noCopy 用来阻止 sync.Map 在使用后被值拷贝。sync.Map 不是普通容器,而是带有并发协议的状态机,内部的锁、原子变量和多视图状态必须保持一致,一旦拷贝就会破坏这些不变量。
- read atomic.Pointer[readOnly] 只读 map,可以不加锁读
- dirty map[any]*entry 加锁处理的可读写 map ,dirty != nil 时 dirty 是完整视图,read 在 amended=true 时可能缺新 key,并且它们指向的是同一个 entry 对象。
- misses int 记录访问 read 的未命中次数,达到阈值则 dirty map 会被提升为新的 read map
- 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 的指针,有三种状态:
- 指向实际的值
- 删除操作第一阶段会将 p 原子地设为 nil
- 在 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 的方法:
- 首先调用 loadReadOnly()原子操作读取 m.read,如果没有则返回一个readOnly{}
- 判断 read 中是否有 key 对应的 entry,有的话直接返回
- 如果没有且 amended 为 false,直接返回 nil,false
- 否则对 m 加锁,再次查询 read, 还是没有且 amended 为 true 则需要去 dirty 查询,此时 read 的未命中调用 missLocked 对 misses++
- 解锁返回查询结果。
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 进行原子加载
- p == nil || p == expunged 则代表,key 已经被删除返回 nil
- 否则读 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
}
- 如果 read 中有对应的 key 存在,就进入 trySwap 判断,在 trySwap 函数里,会判断现在 entry.p 是否是 expunged ,如果不是则直接基于 CAS 操作进行 entry 值的更新,然后返回
- 若前面没有更新,则对 m 进行加锁,进行二次判断 read 中是否存在对应 key
- 存在的话,首先调用 unexpungeLocked 原子操作判断 entry.p 不是 expunged 返回 false 则直接原子更新 entry 为新值;如果 entry.p 是 expunged 则将 entry.p 置为 nil 返回 true 补齐 dirty 的 key-entry ,然后原子更新值
- 如果 read 中不存在对应的 key 且 dirty 中存在对应的 key 则直接原子更新 dirty 的 entry 为新值
- 如果 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 赋值
- 解锁返回
*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
- 不为 nil,直接返回
- 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 则执行该函数
- 如果 entry.p 为 nil 则原子操作改为 expunged
- 返回 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
}
- 首先读取 read map,如果 read map 存在对应的 key-entry 直接调用 e.delete 返回
- 如果 read map 不存在对应得 key-entry 且 amended 为 false,直接返回
- 如果 amended 为 true 则加锁,二次检查 read map,如果 read map 此时存在对应 key-entry 则解锁,调用 e.delete 返回
- 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 得值执行不同操作
- p == nil || p == expunged,说明 entry 已经处于删除态,直接返回
- 否则将 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(©Read)
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…
谢谢阅读!