Golang sync.Map 深入探究

欢迎各位同学关注我哦~
在这个 AI 喧嚣的时代
不忘初心,戒骄戒躁,认真沉淀
Deepincode

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 在读多写少的场景下性能不错,但有两个问题:

  1. 写锁阻塞读锁:即使写的是不同的 key,也会阻塞所有读操作
  2. 锁竞争:高并发下,锁成为瓶颈

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
}

关键点:

  1. 快路径:先从 read 无锁读取,命中则直接返回
  2. 慢路径read 未命中且 amended 为 true,加锁查 dirty
  3. 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
}

关键逻辑:

  1. 新建 dirty 时,遍历 read.m
  2. nil 状态的 entry 标记为 expunged
  3. 只有非 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 喧嚣的时代
不忘初心,戒骄戒躁,认真沉淀
Deepincode
posted @ 2026-04-01 01:19  观千秋皆似我  阅读(4)  评论(0)    收藏  举报