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

8.9 KiB
Raw Blame History

tags, create time
tags create time
go
golang
go-principle
sync-map
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 的整体架构

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 的不同值表示不同的删除状态:

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:先试无锁,失败再加锁

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()
}

流程概括:

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:优先无锁读

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 提升:

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 立即删除

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:遍历前先确保一致性

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 的状态变化:

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 更合适

关联笔记