Go 源码深度解析之 sync.Map(数据结构/读写删/使用场景)
一、简介
写在前面:先写个结论,让大家对该结构的源码有个大概了解,之后再一步一步解析源码:
-
sync.Map 是并发安全的,内部采用读(read)写(dirty)分离,常用于
多读少写场景 -
不适用存储大量数据,因为最坏情况下,sync.Map 的内存会翻倍
-
设计理念
-
读写分离
read:只读结构,采用原子操作,无须加锁,提高性能
dirty:普通map,读写都加锁
misses:从 dirty 中读取的次数,misses>=len(dirty) 时 dirty 升级为 read(直接指针赋值)
读: read 中读(原子操作,无锁),不存在,则上锁从 dirty 中读,且 misses++,misses>=len(dirty) 则 升级为 read(指针赋值)
写:上锁写入 dirty
-
延迟删除
删除 read 中的元素只是将 entry.p 标记为 nil(软删除),在 dirty 重建时(dirtyLocked),nil 状态会被进一步标记为 expunged 并跳过,从而实现真正的清理
-
降低锁的粒度
使用只读结构 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:已删除,尚未从 dirty 中清除;2. expunged:已删除,且已从 dirty 中移除;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 读取 value,ok
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()
当 dirty 为 nil 时(比如刚发生过 dirty 提升为 read),需要重建 dirty。
之前执行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++ && 判断是否需要提升 dirty 为 read
}
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 需要加锁
- 新增全新的 key 时,如果 dirty == nil,还会将 read 中所有未删除数据复制到 dirty,这个过程是加锁的,耗时
- 频繁写入导致 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 升级 read 的时候,dirty 是直接全量覆盖 read 结构的,所以 dirty 的数据必须是全量的,
升级后会重置 dirty=nil,直到有新元素插入才会将 read 结构复制到 dirty
之所以 dirty 的数据必须是完整的:因为 dirty 提升为 read 时是直接指针赋值(m.read.Store(readOnly{m: m.dirty})),提升后 dirty 被置为 nil。如果 dirty 不包含全量数据,提升后 read 中就会丢失数据。而写操作时先尝试 CAS 更新 read 中已有的 entry(无锁,高性能),失败后才加锁更新 dirty,这样可以最大限度地减少锁竞争
8. sync.Map 最坏情况下内存翻倍
为了能将 dirty 中的数据快速提升为 readonly,dirty 中包含了 readonly 中的全部数据,意味着最坏情况下内存占用翻倍,
如果存储了大量数据的话,需要关注这点
总结
sync.Map 作为 Go 内置的并发安全 map,其核心设计是“读写分离 + dirty 提升”:通过 read 承载无锁读,dirty 承载加锁写,配合 entry 状态管理实现标记删除,再通过 misses 计数触发 dirty 提升,将最新数据同步到 read。这种设计让它在“读多写少”“高并发”场景下性能远超普通 map 加锁的方案。
使用 sync.Map 时需明确其适用场景:优先用于缓存、配置存储等读多写少的并发场景,避免在写多或数据量极少的场景中使用。
理解其底层的 read/dirty 结构、提升机制和标记删除逻辑,能帮助我们更合理地选择并发数据结构,写出高效且安全的 Go 并发代码。
如果大家关于 go sync.map 的源码解读还有哪些不清楚的地方,欢迎大家在评论区交流~~~
版权声明
未经授权,禁止转载本文章。
如需转载请保留原文链接并注明出处。即视为默认获得授权。
未保留原文链接未注明出处或删除链接将视为侵权,必追究法律责任!
本文原文链接: https://fiveyoboy.com/articles/go-source-code-syncmap/
备用原文链接: https://blog.fiveyoboy.com/articles/go-source-code-syncmap/