Golang sync.Map 深入探究
|
欢迎各位同学关注我哦~
在这个 AI 喧嚣的时代 不忘初心,戒骄戒躁,认真沉淀 |
|
Go 1.9 引入了 sync.Map,为并发场景提供了一种高性能的 map 实现。但它的设计思路与普通的 map + sync.RWMutex 截然不同,这篇文章从源码层面拆解它的实现原理。
一、为什么需要 sync.Map?
普通的 map 在并发读写时会 panic,必须配合锁使用:
var mu sync.RWMutex
var m = make(map[string]int)
// 读
mu.RLock()
v := m["key"]
mu.RUnlock()
// 写
mu.Lock()
m["key"] = 1
mu.Unlock()
RWMutex 在读多写少的场景下性能不错,但有两个问题:
- 写锁阻塞读锁:即使写的是不同的 key,也会阻塞所有读操作
- 锁竞争:高并发下,锁成为瓶颈
sync.Map 的设计目标是:
| 场景 | sync.Map 优势 |
|---|---|
| key 只写一次,多次读取 | 读操作无锁,直接走 atomic |
| 不同 goroutine 操作不同 key | 减少锁竞争,提高并行度 |
二、核心数据结构
2.1 Map 结构
// sync/map.go
type Map struct {
mu Mutex
// read 包含 map 中可以安全并发访问的部分
// 总是可以安全加载,但存储时必须持有 mu
read atomic.Value // readOnly
// dirty 包含 map 中需要持有 mu 才能访问的部分
// 为了快速提升为 read,它也包含 read 中所有未被 expunged 的 entry
dirty map[any]*entry
// misses 统计从 read 读取失败需要加锁访问 dirty 的次数
// 当 misses 达到一定阈值,dirty 会被提升为 read
misses int
}
2.2 readOnly 结构
type readOnly struct {
m map[any]*entry
amended bool // dirty 包含 read.m 中没有的 key
}
2.3 entry 结构
// expunged 是一个特殊指针,标记 entry 已从 dirty 中删除
var expunged = unsafe.Pointer(new(any))
type entry struct {
// p 指向存储的值,有三种状态:
// nil: 已删除,dirty == nil 或 dirty[key] == e
// expunged: 已删除,dirty != nil,但 entry 不在 dirty 中
// 其他: 有效值,同时存在于 read.m[key] 和 dirty[key]
p unsafe.Pointer // *interface{}
}
entry 的三种状态:
| p 值 | 含义 | 在 dirty 中 |
|---|---|---|
| nil | 已删除 | 可能存在 |
| expunged | 已删除且被清理 | 不存在 |
| 有效指针 | 有效值 | 存在 |
2.4 整体架构
┌─────────────────────────────────────────────────────────────┐
│ Map │
├─────────────────────────────────────────────────────────────┤
│ mu (Mutex) │
├─────────────────────────────────────────────────────────────┤
│ read (atomic.Value) ──► readOnly │
│ ├─ m: map[any]*entry │
│ └─ amended: bool │
├─────────────────────────────────────────────────────────────┤
│ dirty: map[any]*entry │
│ (包含 read 中未 expunged 的 entry + 新增的 entry) │
├─────────────────────────────────────────────────────────────┤
│ misses: int │
└─────────────────────────────────────────────────────────────┘
三、Load:读操作
func (m *Map) Load(key any) (value any, ok bool) {
// 1. 从 read 读取(无锁)
read, _ := m.read.Load().(readOnly)
e, ok := read.m[key]
// 2. read 没有,但 amended 为 true,说明 dirty 可能有
if !ok && read.amended {
m.mu.Lock()
// 双重检查:加锁期间可能 dirty 已提升为 read
read, _ = m.read.Load().(readOnly)
e, ok = read.m[key]
if !ok && read.amended {
// 3. 从 dirty 读取
e, ok = m.dirty[key]
// 记录 miss
m.missLocked()
}
m.mu.Unlock()
}
if !ok {
return nil, false
}
// 4. 从 entry 加载值
return e.load()
}
func (e *entry) load() (value any, ok bool) {
p := atomic.LoadPointer(&e.p)
if p == nil || p == expunged {
return nil, false
}
return *(*any)(p), true
}
关键点:
- 快路径:先从
read无锁读取,命中则直接返回 - 慢路径:
read未命中且amended为 true,加锁查dirty - miss 计数:每次从
dirty读取成功,都会调用missLocked()
3.1 missLocked:miss 计数与 dirty 提升
func (m *Map) missLocked() {
m.misses++
// misses 达到 dirty 长度时,提升 dirty 为 read
if m.misses < len(m.dirty) {
return
}
m.read.Store(readOnly{m: m.dirty})
m.dirty = nil
m.misses = 0
}
提升时机:当 miss 次数等于 dirty 的元素数量时。这意味着如果 dirty 的元素经常被访问,就会很快提升,之后的读取就变成无锁了。
四、Store:写操作
func (m *Map) Store(key, value any) {
// 1. 快路径:read 中已存在该 key,尝试 CAS 更新
read, _ := m.read.Load().(readOnly)
if e, ok := read.m[key]; ok && e.tryStore(&value) {
return
}
m.mu.Lock()
read, _ = m.read.Load().(readOnly)
// 2. read 中存在该 key
if e, ok := read.m[key]; ok {
// 如果 entry 是 expunged,需要恢复到 dirty
if e.unexpungeLocked() {
m.dirty[key] = e
}
e.storeLocked(&value)
} else if e, ok := m.dirty[key]; ok {
// 3. dirty 中存在该 key,直接更新
e.storeLocked(&value)
} else {
// 4. 新 key
if !read.amended {
// dirty 为 nil,需要初始化
m.dirtyLocked()
m.read.Store(readOnly{m: read.m, amended: true})
}
m.dirty[key] = newEntry(value)
}
m.mu.Unlock()
}
4.1 tryStore:CAS 更新
func (e *entry) tryStore(i *any) bool {
for {
p := atomic.LoadPointer(&e.p)
// 如果是 expunged,需要加锁处理
if p == expunged {
return false
}
// CAS 更新
if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
return true
}
}
}
4.2 unexpungeLocked:恢复 expunged 状态
func (e *entry) unexpungeLocked() (wasExpunged bool) {
return atomic.CompareAndSwapPointer(&e.p, expunged, nil)
}
4.3 dirtyLocked:初始化 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 {
// expunged 的 entry 不加入 dirty
if !e.tryExpungeLocked() {
m.dirty[k] = e
}
}
}
func (e *entry) tryExpungeLocked() (isExpunged bool) {
p := atomic.LoadPointer(&e.p)
for p == nil {
// nil 转换为 expunged
if atomic.CompareAndSwapPointer(&e.p, nil, expunged) {
return true
}
p = atomic.LoadPointer(&e.p)
}
return p == expunged
}
关键逻辑:
- 新建
dirty时,遍历read.m - 将
nil状态的 entry 标记为expunged - 只有非
expunged的 entry 才加入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 {
m.mu.Lock()
read, _ = m.read.Load().(readOnly)
e, ok = read.m[key]
if !ok && read.amended {
e, ok = m.dirty[key]
// 从 dirty 中删除
delete(m.dirty, key)
m.missLocked()
}
m.mu.Unlock()
}
if ok {
return e.delete()
}
return nil, false
}
func (e *entry) delete() (value any, ok bool) {
for {
p := atomic.LoadPointer(&e.p)
if p == nil || p == expunged {
return nil, false
}
// CAS 设置为 nil
if atomic.CompareAndSwapPointer(&e.p, p, nil) {
return *(*any)(p), true
}
}
}
删除的两种情况:
| 场景 | 操作 |
|---|---|
| entry 在 read 中 | CAS 将 p 设为 nil |
| entry 只在 dirty 中 | 直接 delete(m.dirty, key) |
六、Range:遍历操作
func (m *Map) Range(f func(key, value any) bool) {
read, _ := m.read.Load().(readOnly)
if read.amended {
// dirty 有新数据,先提升为 read
m.mu.Lock()
read, _ = m.read.Load().(readOnly)
if read.amended {
read = readOnly{m: m.dirty}
m.read.Store(read)
m.dirty = nil
m.misses = 0
}
m.mu.Unlock()
}
// 遍历 read.m
for k, e := range read.m {
v, ok := e.load()
if !ok {
continue
}
if !f(k, v) {
break
}
}
}
Range 会触发 dirty 提升为 read,保证遍历到所有数据。
七、LoadOrStore:原子操作
func (m *Map) LoadOrStore(key, value any) (actual any, loaded bool) {
// 快路径
read, _ := m.read.Load().(readOnly)
if e, ok := read.m[key]; ok {
actual, loaded, ok := e.tryLoadOrStore(value)
if ok {
return actual, loaded
}
}
m.mu.Lock()
// ... 类似 Store 的逻辑
}
func (e *entry) tryLoadOrStore(i any) (actual any, loaded, ok bool) {
p := atomic.LoadPointer(&e.p)
if p == expunged {
return nil, false, false
}
if p != nil {
// 已有值,返回
return *(*any)(p), true, true
}
// p == nil,尝试存储
ic := i
for {
if atomic.CompareAndSwapPointer(&e.p, nil, unsafe.Pointer(&ic)) {
return i, false, true
}
p = atomic.LoadPointer(&e.p)
if p == expunged {
return nil, false, false
}
if p != nil {
return *(*any)(p), true, true
}
}
}
八、状态转换图
entry 状态转换:
新建 Store
│
▼
┌─────────┐ Store 已有 key ┌─────────┐
│ 有效值 │◄────────────────────│ 有效值 │
└────┬────┘ └─────────┘
│ ▲
│ Delete │ Store
▼ │
┌───┐ ┌─────┴─────┐
│nil│◄────────────────────────│unexpunge │
└─┬─┘ expunge │(加锁时) │
│ dirtyLocked() └───────────┘
│ ▲
▼ │
┌──────────┐ Store 已有 key ┌──────┴──────┐
│ expunged │─────────────────►│ nil │
└──────────┘ (加锁 unexpunge) └─────────────┘
九、流程图
9.1 Load 流程
Load(key)
│
└─► read.m[key] 存在?
│
├─► 是 ──► entry.load() ──► 返回
│
└─► 否 ──► read.amended?
│
├─► false ──► 返回 nil, false
│
└─► true ──► 加锁
│
├─► 双重检查 read.m[key]
│
├─► dirty[key] 存在?
│ │
│ ├─► 是 ──► missLocked()
│ │
│ └─► 否 ──► missLocked()
│
└─► 解锁 ──► entry.load()
9.2 Store 流程
Store(key, value)
│
├─► read.m[key] 存在?
│ │
│ ├─► 是 ──► tryStore 成功?
│ │ │
│ │ ├─► 是 ──► 返回
│ │ │
│ │ └─► 否 ──► 继续加锁
│ │
│ └─► 否 ──► 继续加锁
│
└─► 加锁
│
├─► read.m[key] 存在?
│ │
│ ├─► 是 ──► unexpungeLocked()
│ │ │
│ │ ├─► 是 expunged ──► 加入 dirty
│ │ │
│ │ └─► storeLocked()
│ │
│ ├─► dirty[key] 存在?
│ │ │
│ │ └─► 是 ──► storeLocked()
│ │
│ └─► 新 key ──► dirty 为空?
│ │
│ ├─► 是 ──► dirtyLocked()
│ │ 设置 amended = true
│ │
│ └─► dirty[key] = newEntry()
│
└─► 解锁
sync.Map的核心思想是空间换时间,通过维护两个 map(read 和 dirty),读优先走 read(无锁),写走 dirty(有锁)。
sync.Map 通过读写分离、延迟删除、按需提升等策略,在特定场景下显著降低了锁竞争。但它不是银弹,理解其设计原理,才能在正确的场景使用它。
|
欢迎各位同学关注我哦~
在这个 AI 喧嚣的时代 不忘初心,戒骄戒躁,认真沉淀 |
|
浙公网安备 33010602011771号