找回密码
 立即注册
首页 业界区 安全 Golang sync.Map 深入探究

Golang sync.Map 深入探究

吕梓美 昨天 23:55
              欢迎各位同学关注我哦~
        在这个 AI 喧嚣的时代
        不忘初心,戒骄戒躁,认真沉淀         
      
   
Go 1.9 引入了 sync.Map,为并发场景提供了一种高性能的 map 实现。但它的设计思路与普通的 map + sync.RWMutex 截然不同,这篇文章从源码层面拆解它的实现原理。
一、为什么需要 sync.Map?

普通的 map 在并发读写时会 panic,必须配合锁使用:
  1. var mu sync.RWMutex
  2. var m = make(map[string]int)
  3. // 读
  4. mu.RLock()
  5. v := m["key"]
  6. mu.RUnlock()
  7. // 写
  8. mu.Lock()
  9. m["key"] = 1
  10. mu.Unlock()
复制代码
RWMutex 在读多写少的场景下性能不错,但有两个问题:

  • 写锁阻塞读锁:即使写的是不同的 key,也会阻塞所有读操作
  • 锁竞争:高并发下,锁成为瓶颈
sync.Map 的设计目标是:
场景sync.Map 优势key 只写一次,多次读取读操作无锁,直接走 atomic不同 goroutine 操作不同 key减少锁竞争,提高并行度二、核心数据结构

2.1 Map 结构
  1. // sync/map.go
  2. type Map struct {
  3.     mu Mutex
  4.     // read 包含 map 中可以安全并发访问的部分
  5.     // 总是可以安全加载,但存储时必须持有 mu
  6.     read atomic.Value // readOnly
  7.     // dirty 包含 map 中需要持有 mu 才能访问的部分
  8.     // 为了快速提升为 read,它也包含 read 中所有未被 expunged 的 entry
  9.     dirty map[any]*entry
  10.     // misses 统计从 read 读取失败需要加锁访问 dirty 的次数
  11.     // 当 misses 达到一定阈值,dirty 会被提升为 read
  12.     misses int
  13. }
复制代码
2.2 readOnly 结构
  1. type readOnly struct {
  2.     m       map[any]*entry
  3.     amended bool // dirty 包含 read.m 中没有的 key
  4. }
复制代码
2.3 entry 结构
  1. // expunged 是一个特殊指针,标记 entry 已从 dirty 中删除
  2. var expunged = unsafe.Pointer(new(any))
  3. type entry struct {
  4.     // p 指向存储的值,有三种状态:
  5.     // nil: 已删除,dirty == nil 或 dirty[key] == e
  6.     // expunged: 已删除,dirty != nil,但 entry 不在 dirty 中
  7.     // 其他: 有效值,同时存在于 read.m[key] 和 dirty[key]
  8.     p unsafe.Pointer // *interface{}
  9. }
复制代码
entry 的三种状态:
p 值含义在 dirty 中nil已删除可能存在expunged已删除且被清理不存在有效指针有效值存在2.4 整体架构
  1. ┌─────────────────────────────────────────────────────────────┐
  2. │                          Map                                │
  3. ├─────────────────────────────────────────────────────────────┤
  4. │  mu (Mutex)                                                 │
  5. ├─────────────────────────────────────────────────────────────┤
  6. │  read (atomic.Value) ──► readOnly                           │
  7. │                            ├─ m: map[any]*entry             │
  8. │                            └─ amended: bool                 │
  9. ├─────────────────────────────────────────────────────────────┤
  10. │  dirty: map[any]*entry                                      │
  11. │    (包含 read 中未 expunged 的 entry + 新增的 entry)           │
  12. ├─────────────────────────────────────────────────────────────┤
  13. │  misses: int                                                │
  14. └─────────────────────────────────────────────────────────────┘
复制代码
三、Load:读操作
  1. func (m *Map) Load(key any) (value any, ok bool) {
  2.     // 1. 从 read 读取(无锁)
  3.     read, _ := m.read.Load().(readOnly)
  4.     e, ok := read.m[key]
  5.    
  6.     // 2. read 没有,但 amended 为 true,说明 dirty 可能有
  7.     if !ok && read.amended {
  8.         m.mu.Lock()
  9.         // 双重检查:加锁期间可能 dirty 已提升为 read
  10.         read, _ = m.read.Load().(readOnly)
  11.         e, ok = read.m[key]
  12.         if !ok && read.amended {
  13.             // 3. 从 dirty 读取
  14.             e, ok = m.dirty[key]
  15.             // 记录 miss
  16.             m.missLocked()
  17.         }
  18.         m.mu.Unlock()
  19.     }
  20.     if !ok {
  21.         return nil, false
  22.     }
  23.     // 4. 从 entry 加载值
  24.     return e.load()
  25. }
  26. func (e *entry) load() (value any, ok bool) {
  27.     p := atomic.LoadPointer(&e.p)
  28.     if p == nil || p == expunged {
  29.         return nil, false
  30.     }
  31.     return *(*any)(p), true
  32. }
复制代码
关键点:

  • 快路径:先从 read 无锁读取,命中则直接返回
  • 慢路径:read 未命中且 amended 为 true,加锁查 dirty
  • miss 计数:每次从 dirty 读取成功,都会调用 missLocked()
3.1 missLocked:miss 计数与 dirty 提升
  1. func (m *Map) missLocked() {
  2.     m.misses++
  3.     // misses 达到 dirty 长度时,提升 dirty 为 read
  4.     if m.misses < len(m.dirty) {
  5.         return
  6.     }
  7.     m.read.Store(readOnly{m: m.dirty})
  8.     m.dirty = nil
  9.     m.misses = 0
  10. }
复制代码
提升时机:当 miss 次数等于 dirty 的元素数量时。这意味着如果 dirty 的元素经常被访问,就会很快提升,之后的读取就变成无锁了。
四、Store:写操作
  1. func (m *Map) Store(key, value any) {
  2.     // 1. 快路径:read 中已存在该 key,尝试 CAS 更新
  3.     read, _ := m.read.Load().(readOnly)
  4.     if e, ok := read.m[key]; ok && e.tryStore(&value) {
  5.         return
  6.     }
  7.     m.mu.Lock()
  8.     read, _ = m.read.Load().(readOnly)
  9.    
  10.     // 2. read 中存在该 key
  11.     if e, ok := read.m[key]; ok {
  12.         // 如果 entry 是 expunged,需要恢复到 dirty
  13.         if e.unexpungeLocked() {
  14.             m.dirty[key] = e
  15.         }
  16.         e.storeLocked(&value)
  17.     } else if e, ok := m.dirty[key]; ok {
  18.         // 3. dirty 中存在该 key,直接更新
  19.         e.storeLocked(&value)
  20.     } else {
  21.         // 4. 新 key
  22.         if !read.amended {
  23.             // dirty 为 nil,需要初始化
  24.             m.dirtyLocked()
  25.             m.read.Store(readOnly{m: read.m, amended: true})
  26.         }
  27.         m.dirty[key] = newEntry(value)
  28.     }
  29.     m.mu.Unlock()
  30. }
复制代码
4.1 tryStore:CAS 更新
  1. func (e *entry) tryStore(i *any) bool {
  2.     for {
  3.         p := atomic.LoadPointer(&e.p)
  4.         // 如果是 expunged,需要加锁处理
  5.         if p == expunged {
  6.             return false
  7.         }
  8.         // CAS 更新
  9.         if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
  10.             return true
  11.         }
  12.     }
  13. }
复制代码
4.2 unexpungeLocked:恢复 expunged 状态
  1. func (e *entry) unexpungeLocked() (wasExpunged bool) {
  2.     return atomic.CompareAndSwapPointer(&e.p, expunged, nil)
  3. }
复制代码
4.3 dirtyLocked:初始化 dirty
  1. func (m *Map) dirtyLocked() {
  2.     if m.dirty != nil {
  3.         return
  4.     }
  5.     read, _ := m.read.Load().(readOnly)
  6.     m.dirty = make(map[any]*entry, len(read.m))
  7.     for k, e := range read.m {
  8.         // expunged 的 entry 不加入 dirty
  9.         if !e.tryExpungeLocked() {
  10.             m.dirty[k] = e
  11.         }
  12.     }
  13. }
  14. func (e *entry) tryExpungeLocked() (isExpunged bool) {
  15.     p := atomic.LoadPointer(&e.p)
  16.     for p == nil {
  17.         // nil 转换为 expunged
  18.         if atomic.CompareAndSwapPointer(&e.p, nil, expunged) {
  19.             return true
  20.         }
  21.         p = atomic.LoadPointer(&e.p)
  22.     }
  23.     return p == expunged
  24. }
复制代码
关键逻辑:

  • 新建 dirty 时,遍历 read.m
  • 将 nil 状态的 entry 标记为 expunged
  • 只有非 expunged 的 entry 才加入 dirty
五、Delete:删除操作
  1. func (m *Map) Delete(key any) {
  2.     m.LoadAndDelete(key)
  3. }
  4. func (m *Map) LoadAndDelete(key any) (value any, loaded bool) {
  5.     read, _ := m.read.Load().(readOnly)
  6.     e, ok := read.m[key]
  7.     if !ok && read.amended {
  8.         m.mu.Lock()
  9.         read, _ = m.read.Load().(readOnly)
  10.         e, ok = read.m[key]
  11.         if !ok && read.amended {
  12.             e, ok = m.dirty[key]
  13.             // 从 dirty 中删除
  14.             delete(m.dirty, key)
  15.             m.missLocked()
  16.         }
  17.         m.mu.Unlock()
  18.     }
  19.     if ok {
  20.         return e.delete()
  21.     }
  22.     return nil, false
  23. }
  24. func (e *entry) delete() (value any, ok bool) {
  25.     for {
  26.         p := atomic.LoadPointer(&e.p)
  27.         if p == nil || p == expunged {
  28.             return nil, false
  29.         }
  30.         // CAS 设置为 nil
  31.         if atomic.CompareAndSwapPointer(&e.p, p, nil) {
  32.             return *(*any)(p), true
  33.         }
  34.     }
  35. }
复制代码
删除的两种情况:
场景操作entry 在 read 中CAS 将 p 设为 nilentry 只在 dirty 中直接 delete(m.dirty, key)六、Range:遍历操作
  1. func (m *Map) Range(f func(key, value any) bool) {
  2.     read, _ := m.read.Load().(readOnly)
  3.     if read.amended {
  4.         // dirty 有新数据,先提升为 read
  5.         m.mu.Lock()
  6.         read, _ = m.read.Load().(readOnly)
  7.         if read.amended {
  8.             read = readOnly{m: m.dirty}
  9.             m.read.Store(read)
  10.             m.dirty = nil
  11.             m.misses = 0
  12.         }
  13.         m.mu.Unlock()
  14.     }
  15.     // 遍历 read.m
  16.     for k, e := range read.m {
  17.         v, ok := e.load()
  18.         if !ok {
  19.             continue
  20.         }
  21.         if !f(k, v) {
  22.             break
  23.         }
  24.     }
  25. }
复制代码
Range 会触发 dirty 提升为 read,保证遍历到所有数据。
七、LoadOrStore:原子操作
  1. func (m *Map) LoadOrStore(key, value any) (actual any, loaded bool) {
  2.     // 快路径
  3.     read, _ := m.read.Load().(readOnly)
  4.     if e, ok := read.m[key]; ok {
  5.         actual, loaded, ok := e.tryLoadOrStore(value)
  6.         if ok {
  7.             return actual, loaded
  8.         }
  9.     }
  10.     m.mu.Lock()
  11.     // ... 类似 Store 的逻辑
  12. }
  13. func (e *entry) tryLoadOrStore(i any) (actual any, loaded, ok bool) {
  14.     p := atomic.LoadPointer(&e.p)
  15.     if p == expunged {
  16.         return nil, false, false
  17.     }
  18.     if p != nil {
  19.         // 已有值,返回
  20.         return *(*any)(p), true, true
  21.     }
  22.     // p == nil,尝试存储
  23.     ic := i
  24.     for {
  25.         if atomic.CompareAndSwapPointer(&e.p, nil, unsafe.Pointer(&ic)) {
  26.             return i, false, true
  27.         }
  28.         p = atomic.LoadPointer(&e.p)
  29.         if p == expunged {
  30.             return nil, false, false
  31.         }
  32.         if p != nil {
  33.             return *(*any)(p), true, true
  34.         }
  35.     }
  36. }
复制代码
八、状态转换图
  1. entry 状态转换:
  2.   新建 Store
  3.      │
  4.      ▼
  5. ┌─────────┐    Store 已有 key    ┌─────────┐
  6. │  有效值  │◄────────────────────│  有效值  │
  7. └────┬────┘                      └─────────┘
  8.      │                                 ▲
  9.      │ Delete                          │ Store
  10.      ▼                                 │
  11.    ┌───┐                         ┌─────┴─────┐
  12.    │nil│◄────────────────────────│unexpunge  │
  13.    └─┬─┘        expunge          │(加锁时)    │
  14.      │     dirtyLocked()         └───────────┘
  15.      │                                ▲
  16.      ▼                                │
  17. ┌──────────┐  Store 已有 key   ┌──────┴──────┐
  18. │ expunged │─────────────────►│   nil       │
  19. └──────────┘ (加锁 unexpunge)  └─────────────┘
复制代码
九、流程图

9.1 Load 流程
  1. Load(key)
  2.     │
  3.     └─► read.m[key] 存在?
  4.             │
  5.             ├─► 是 ──► entry.load() ──► 返回
  6.             │
  7.             └─► 否 ──► read.amended?
  8.                          │
  9.                          ├─► false ──► 返回 nil, false
  10.                          │
  11.                          └─► true ──► 加锁
  12.                                          │
  13.                                          ├─► 双重检查 read.m[key]
  14.                                          │
  15.                                          ├─► dirty[key] 存在?
  16.                                          │       │
  17.                                          │       ├─► 是 ──► missLocked()
  18.                                          │       │
  19.                                          │       └─► 否 ──► missLocked()
  20.                                          │
  21.                                          └─► 解锁 ──► entry.load()
复制代码
9.2 Store 流程
  1. Store(key, value)
  2.     │
  3.     ├─► read.m[key] 存在?
  4.     │       │
  5.     │       ├─► 是 ──► tryStore 成功?
  6.     │       │               │
  7.     │       │               ├─► 是 ──► 返回
  8.     │       │               │
  9.     │       │               └─► 否 ──► 继续加锁
  10.     │       │
  11.     │       └─► 否 ──► 继续加锁
  12.     │
  13.     └─► 加锁
  14.             │
  15.             ├─► read.m[key] 存在?
  16.             │       │
  17.             │       ├─► 是 ──► unexpungeLocked()
  18.             │       │           │
  19.             │       │           ├─► 是 expunged ──► 加入 dirty
  20.             │       │           │
  21.             │       │           └─► storeLocked()
  22.             │       │
  23.             │       ├─► dirty[key] 存在?
  24.             │       │       │
  25.             │       │       └─► 是 ──► storeLocked()
  26.             │       │
  27.             │       └─► 新 key ──► dirty 为空?
  28.             │                       │
  29.             │                       ├─► 是 ──► dirtyLocked()
  30.             │                       │          设置 amended = true
  31.             │                       │
  32.             │                       └─► dirty[key] = newEntry()
  33.             │
  34.             └─► 解锁
复制代码
sync.Map的核心思想是空间换时间,通过维护两个 map(read 和 dirty),读优先走 read(无锁),写走 dirty(有锁)。
sync.Map 通过读写分离、延迟删除、按需提升等策略,在特定场景下显著降低了锁竞争。但它不是银弹,理解其设计原理,才能在正确的场景使用它。
              欢迎各位同学关注我哦~
        在这个 AI 喧嚣的时代
        不忘初心,戒骄戒躁,认真沉淀         
      
   

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册