Files
cs-note/hzh/GolangStar/Go语言原理/sync.map原理.md
T

271 lines
8.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [go, golang, go-principle, sync-map]
create time: 2026-06-07 15:20
---
# Sync.Map 底层原理
## 概述
本文深入解析 `sync.Map` 的三表架构(read / dirty / amended),以及 Store / Load / Delete / Range 四个核心操作的源码实现。与原生 map 不同,sync.Map 采用空间换时间的策略,在特定场景下(读多写少)性能远超加锁的原生 map。
> [!question] ❓ 思考
> 为什么 sync.Map 要设计 read 和 dirty 两个 map?如果一个 key 同时存在于两者中,它们指向的是同一个 value 吗?expunged 状态存在的意义是什么?
## 正文
### 一、Sync.Map 的整体架构
```mermaid
graph TB
subgraph Map["sync.Map"]
mu["mu: Mutex<br/>保护 dirty"]
read["read: atomic.Value<br/>readOnly{m, amended}"]
dirty["dirty: map[key]*entry<br/>需加锁访问"]
misses["misses: int<br/>未命中计数器"]
end
subgraph ReadOnly
rm["m: map[key]*entry"]
am["amended: bool"]
end
subgraph Entry["entry.p 三种状态"]
nil_s["nil = 软删除<br/>key 只存在于 read"]
exp["expunged = 假删除<br/>key 在 read 但不在 dirty"]
val["正常值 = p → &value"]
end
read --> ReadOnly
ReadOnly --> rm
ReadOnly --> am
dirty --> Entry
mu -.保护.-> dirty
misses -.计数.-> dirty
style Map fill:#e3f2fd
style read fill:#e8f5e9
style dirty fill:#fff9c4
```
#### 核心字段说明
| 字段 | 类型 | 作用 |
|------|------|------|
| `mu` | `Mutex` | 保护 `dirty` map 的互斥锁 |
| `read` | `atomic.Value` | 原子读取的只读 map,类型为 `readOnly` |
| `dirty` | `map[interface{}]*entry` | 需要加锁的可变 map |
| `misses` | `int` | read 未命中次数,达到 dirty 长度时触发提升 |
> [!tip] 💡 理解要点
> read 中的 `*entry` 和 dirty 中的 `*entry` 可能指向**同一个 entry 对象**。所以通过 read 修改 value 后,dirty 也能读到新值。
### 二、Entry 的三种状态
这是 sync.Map 最精妙的设计之一——用 `entry.p` 的不同值表示不同的删除状态:
```go
type entry struct {
p unsafe.Pointer // 指向真实 value
}
```
| 状态 | p 的值 | 含义 | dirty 状态 |
|------|--------|------|-----------|
| 正常 | `&value` | 有效数据 | 包含该 key |
| 软删除 | `nil` | key 被删除,但未重建 dirty | dirty 可能为 nil 或包含该 key |
| 假删除 | `expunged` | key 在 read 中但不存在于 dirty | 不包含该 key,且 dirty 中有其他 read 没有的 key |
> [!note] 📝 为什么需要 expunged?
> 如果只有 nil 表示删除,当从 read 读取到 nil 状态的 entry 时,无法判断 dirty 中是否也有这个 key。有了 expunged 后:nil 表示"不用查 dirty",expunged 表示"必须查 dirty",从而避免不必要的加锁。
### 三、核心操作源码解析
#### 3.1 Store:先试无锁,失败再加锁
```go
func (m *Map) Store(key, value any) {
// 第一步:无锁尝试——直接 CAS 更新 read 中的 entry
read, _ := m.read.Load().(readOnly)
if e, ok := read.m[key]; ok && e.tryStore(&value) {
return // 成功!全程无锁
}
// 第二步:加锁,处理各种边界情况
m.mu.Lock()
read, _ = m.read.Load().(readOnly) // 双重检查
if e, ok := read.m[key]; ok {
if e.unexpungeLocked() { // expunged → nil
m.dirty[key] = e // 加入 dirty
}
e.storeLocked(&value) // 更新 value
} else if e, ok := m.dirty[key]; ok {
e.storeLocked(&value) // dirty 中已有,直接更新
} else { // 全新 key
if !read.amended {
m.dirtyLocked() // 根据 read 重建 dirty
m.read.Store(readOnly{m: read.m, amended: true})
}
m.dirty[key] = newEntry(value) // 写入 dirty
}
m.mu.Unlock()
}
```
流程概括:
```mermaid
flowchart TD
Start["Store key, value"] --> LockFree{"read 中存在且非 expunged?"}
LockFree -->|是| CAS["CAS 原子更新 entry.p"]
CAS --> Done["完成,无锁"]
LockFree -->|否| Lock["加锁 mu"]
Lock --> ReadExist{"read 中存在?"}
ReadExist -->|是| HandleExpunged{"p == expunged?"}
HandleExpunged -->|是| AddToDirty["expunged→nil, 加入 dirty"]
HandleExpunged -->|否| UpdateRead["更新 read 中的 entry"]
ReadExist -->|否| DirtyExist{"dirty 中存在?"}
DirtyExist -->|是| UpdateDirty["更新 dirty 中的 entry"]
DirtyExist -->|否| NewKey{"amended 标记?"}
NewKey -->|false| RebuildDirty["重建 dirty + 设 amended=true"]
NewKey -->|true| WriteDirty["直接写入 dirty"]
AddToDirty --> Unlock["解锁"]
UpdateRead --> Unlock
UpdateDirty --> Unlock
RebuildDirty --> Unlock
WriteDirty --> Unlock
Unlock --> Done
style CAS fill:#e8f5e9
style Lock fill:#fff3e0
```
#### 3.2 Load:优先无锁读
```go
func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
read, _ := m.read.Load().(readOnly)
if e, ok := read.m[key]; ok && e != expunged {
return e.load() // 无锁命中!
}
// 未命中或需要查 dirty
m.mu.Lock()
read, _ = m.read.Load().(readOnly) // 双重检查
if e, ok := read.m[key]; ok && e != expunged {
return e.load()
} else if ok, e := m.dirty[key]; ok {
m.missLocked() // misses++,可能触发 dirty 提升
return e.load()
}
m.mu.Unlock()
return nil, false
}
```
当 `misses >= len(dirty)` 时触发**dirty 提升**:
```go
func (m *Map) missLocked() {
m.misses++
if m.misses >= len(m.dirty) {
m.read.Store(readOnly{m: m.dirty}) // dirty → read
m.dirty = nil // dirty 被 GC
m.misses = 0
}
}
```
#### 3.3 Delete:延迟删除 vs 立即删除
```go
func (m *Map) Delete(key interface{}) {
read, _ := m.read.Load().(readOnly)
if e, ok := read.m[key]; ok {
e.delete() // 软删除:p → nil,不实际移除
return
}
// key 不在 read 中,直接从 dirty 删除
m.mu.Lock()
read, _ = m.read.Load().(readOnly)
if e, ok := read.m[key]; ok {
e.delete()
} else if _, ok = m.dirty[key]; ok {
delete(m.dirty, key) // 真正的删除
}
m.mu.Unlock()
}
```
> [!warning] ⚠️ 注意
> 从 read 中删除是**延迟删除**(只改标记),从 dirty 中删除是**立即删除**(调用 `delete()`)。被软删除的 key 只有在 dirty 提升为 read 时才会真正释放。
#### 3.4 Range:遍历前先确保一致性
```go
func (m *Map) Range(f func(key, value interface{}) bool) {
read, _ := m.read.Load().(readOnly)
if read.amended {
m.mu.Lock()
// 双重检查
read, _ = m.read.Load().(readOnly)
if read.amended {
read = readOnly{m: m.dirty} // 直接用 dirty 遍历
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 }
}
}
```
Range 会先将 dirty 提升到 read,然后对 read 进行无锁遍历,避免了遍历时加锁的性能损失。
### 四、P 状态流转示例
通过一个具体例子来理解 entry.p 的状态变化:
```mermaid
flowchart LR
S1["初始: Store key1, key2 → dirty 有数据, read 为空"]
S2["Load 两次 → misses=2=len(dirty) → dirty 提升为 read"]
S3["Delete key1 → read 中 key1.p=nil"]
S4["Store key3 → dirty 重建: nil→expunged, copy 剩余"]
S5["Store key1 新值 → expunged→nil, 加入 dirty"]
style S1 fill:#e3f2fd
style S2 fill:#fff9c4
style S3 fill:#fff3e0
style S4 fill:#fce4ec
style S5 fill:#e8f5e9
```
### 五、适用场景分析
sync.Map 并非在所有场景都优于原生 map + mutex:
| 场景 | 推荐方案 | 原因 |
|------|---------|------|
| 读多写少 | `sync.Map` | 读操作几乎无锁 |
| 写多读少 | `map + mutex` | sync.Map 的锁竞争成本高 |
| 读写均衡 | `map + RWMutex` | 读写分离更灵活 |
| 每个 key 独立热点 | `分片 map + mutex` | 降低锁粒度 |
> [!info] ℹ️ 补充
> sync.Map 在 Go 1.9 引入,最初设计用于反射等低频访问场景。Go 1.10+ 对其进行了大量优化,才逐渐成为通用并发 map 的首选方案。
## 小结
- sync.Map 通过 read(无锁)+ dirty(加锁)双表结构实现读写分离
- entry.p 的三种状态(正常/nil/expunged)精确控制是否需要加锁
- Store 优先无锁 CAS,Load 优先无锁读,最大限度减少锁竞争
- 适合读多写少的场景;读写均衡时用 map + RWMutex 更合适
## 关联笔记
- [[hzh/GolangStar/Go语言进阶/Sync]] — Sync 包的基础用法
- [[hzh/GolangStar/Go语言基础/Go语言Map]] — Map 的基础用法
- [[hzh/GolangStar/Go面试题库/Sync面试题]] — Sync 同步相关面试题