欢迎各位同学关注我哦~
在这个 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 设为 nilentry 只在 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 喧嚣的时代
不忘初心,戒骄戒躁,认真沉淀 | |
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |