Sirius
Sirius

目录

Golang sync.Map

sync.Map 是 Go 语言标准库 sync 包中提供的一个并发安全的 map。它并非为了替代 map + sync.RWMutex 这种通用模式,而是针对**“读多写少”**的特定场景进行了深度优化。

sync.Map 的设计哲学是读写分离和空间换时间,其最终目的是:

让读操作尽可能快,可以做到在大部分情况下无锁(lock-free)。

sync.Map 的内部实现主要围绕两个核心的 map 结构和一个锁:

  1. read (只读 map)

    • 这是一个 atomic.Pointer,指向一个内部的 readOnly 结构体。readOnly 结构体里包含一个普通的 Go map

    • 这个 read map 存储了 map 中被认为是 “稳定” 的数据 。

    • read map 的访问是原子操作,因此读取是并发安全的,并且不需要加锁。这是 sync.Map 高性能读取的关键。

  2. dirty (可写 map)

    • 这是一个普通的 Go map (map[any]any)。

    • 它存储了最近新增或被修改的键值对。可以把它看作是新数据的“暂存区”或“缓存”。

    • dirty map 的所有访问都必须由一个互斥锁 mu 来保护。

  3. mu (互斥锁 sync.Mutex)

    • 这个锁只用于保护 dirty map 的并发访问。它不保护 read map。
  4. misses (未命中计数器)

    • 用于记录 Load 操作在 read map 中未命中、不得不去查询 dirty map 的次数。当 misses 的数量达到一定阈值(等于 dirty map 的长度)时,就会触发一次数据从 dirtyread 的迁移。
graph TD
    subgraph sm [sync.Map 实例]
        mu["mu (sync.Mutex)"]
        read_ptr["read (atomic.Pointer)"]
        dirty_map["dirty (map[any]any)"]
        misses["misses (int)"]
    end

    subgraph ro [readOnly 结构]
        m["m (map[any]*entry)"]
        amended["amended (bool)"]
    end

    subgraph dirty_map_protected["受 mu 锁保护"]
        dirty_map
    end
    
    mu --- dirty_map_protected

    read_ptr -- "原子加载/存储" --> ro

    style sm fill:#f9f,stroke:#333,stroke-width:2px
    style ro fill:#ccf,stroke:#333,stroke-width:2px
  • sync.Map 实例: 包含一个锁 mu,一个原子指针 read,一个普通的 map dirty 和一个计数器 misses

  • mu (sync.Mutex): 它的保护范围仅限于 dirty map。

  • read (atomic.Pointer): 指向一个 readOnly 结构。所有对 read 的读写都通过原子操作完成,保证并发安全。

  • readOnly 结构: 包含一个 map m,这是读操作的快速路径。amended 标志位表示 dirty map 中是否包含 read map 中没有的数据。

  • dirty map: 是一个常规的 map,存储最新的写入,访问它必须先获取 mu 锁。

这是 sync.Map 被优化的主要路径。

  1. 快速路径(无锁):

    • 通过原子操作加载 read 指针,获取 readOnly map。

    • readOnly.m 中查找 key。

    • 如果找到了,并且其值不是“已删除”状态,直接返回。这个过程完全无锁,非常快。

  2. 慢速路径(加锁):

    • 如果在 read 中没找到,说明 key 可能是新写入的,存在于 dirty 中。

    • 加锁 mu,以安全地访问 dirty map。

    • 再次检查 read map(因为在加锁的瞬间,dirty 可能已经被提升为了新的 read map),防止数据不一致。

    • dirty map 中查找 key,如果找到则返回。

    • 无论是否在 dirty 中找到,都将 misses 计数器加一。

    • 检查 misses 是否达到了 dirty map 的长度,如果是,则触发一次数据迁移。

    • 解锁 mu

写入操作通常是慢路径,因为它很可能需要加锁。

  1. 快速检查:

    • 先不加锁,原子加载 read map,并检查 key 是否存在。

    • 如果 key 存在,尝试通过原子操作 (CAS) 直接更新 read map 中该 key 对应的 entry 的值。如果成功,操作结束。

  2. 慢速路径(加锁):

    • 如果上述快速更新不成功(例如 key 不在 read 中,或 CAS 操作失败),则加锁 mu

    • 再次检查 read map,因为可能在加锁期间发生了变化。

    • 如果 key 在 read 中,但被标记为“已删除”,则需要将其“复活”并存入 dirty

    • 如果 key 不在 read 中,则直接将新的键值对存入 dirty map。

    • 如果 dirty map 为 nil,则会根据 read map 的内容创建一个新的 dirty map,并将新数据存入。

    • 解锁 mu

删除操作总是慢路径,需要加锁。它会将键值对从 dirty map 中删除,并在 read map 中将对应的条目标记为“已删除”(expunged),这是一个逻辑删除,而不是物理删除。

以下是结合代码分析sync.Mapdirty 转换为 read(即 dirty map 提升为 read map

dirty 提升为 read 主要有两种触发情况:

当进行 LoadLoadAndDeleteLoadOrStore 等操作时,如果 read map 中找不到 key,且 read.amended 为 true(表示 dirty 中有 read 没有的数据),就会加锁去 dirty 中查找。

每次需要加锁访问 dirty 时,都会调用 missLocked() 方法。

// src/sync/map.go

func (m *Map) Load(key any) (value any, ok bool) {
    // ... (省略 read 查找)
    m.mu.Lock()
    // ... (双重检查)
    if !ok && read.amended {
        e, ok = m.dirty[key]
        // 记录一次 miss,可能会触发提升
        m.missLocked()
    }
    m.mu.Unlock()
    // ...
}

当调用 Range 遍历 map 时,如果 read.amended 为 true,说明 dirty 中有新数据。为了保证遍历的完整性且不长时间持有锁,sync.Map 会直接将 dirty 提升为 read

// src/sync/map.go

func (m *Map) Range(f func(key, value any) bool) {
    read := m.loadReadOnly()
    if read.amended {
        m.mu.Lock()
        read = m.loadReadOnly()
        if read.amended {
            // 直接提升 dirty 为 read
            read = readOnly{m: m.dirty}
            m.read.Store(&read)
            m.dirty = nil
            m.misses = 0
        }
        m.mu.Unlock()
    }
    // ...
}

这是最核心的自动提升逻辑。

// src/sync/map.go

func (m *Map) missLocked() {
    m.misses++ // 1. 增加 miss 计数
    
    // 2. 判断阈值
    // 如果 miss 次数小于 dirty 的长度,则不提升
    if m.misses < len(m.dirty) {
        return
    }
    
    // 3. 执行提升 (Promotion)
    // 将 dirty map 封装成 readOnly 结构体,原子替换掉 m.read
    // 注意:这里新创建的 readOnly 中 amended 默认为 false
    m.read.Store(&readOnly{m: m.dirty})
    
    // 4. 清理状态
    m.dirty = nil  // dirty 置空
    m.misses = 0   // 计数归零
}
  1. 计数 (m.misses++):

    • 每次在 read 中没找到 key 而被迫加锁去 dirty 找时,misses 计数加 1。
  2. 阈值判断 (m.misses < len(m.dirty)):

    • Go 团队设定的阈值是 len(m.dirty)
    • 逻辑: 如果穿透到 dirty 的次数已经等于 dirty 中元素的个数了,说明 dirty 中的数据被访问得很频繁。此时将 dirty 提升为 read 是划算的,因为以后这些 key 的访问就可以直接走无锁的 read 路径了。
  3. 原子替换 (m.read.Store(...)):

    • m.read 被更新为指向原本的 dirty map。
    • 新的 readOnly 结构体中 amended 字段为 false。这意味着此时 read 包含了所有数据,dirty 中没有额外数据了。
  4. 置空 Dirty (m.dirty = nil):

    • dirty 变为 nil
    • 注意: 下一次发生写操作(Store)时,如果发现 dirty 为 nil,会触发 dirtyLocked,此时会再次把 read 中的有效数据(非 expunged)浅拷贝回 dirty

这是一个非常敏锐的问题!理解这一点的关键在于:dirtyread 虽然是两个不同的 map 结构(map[any]*entry),但它们存储的 value 是指针,指向的是同一个 entry 对象。

简单回答你的问题:

  1. 是两个 mapread.mdirty 是两个独立的 map 对象。
  2. 数据不会丢失:因为 dirty 在创建之初就是 read全量拷贝(除了被标记删除的),并且后续的新增操作都在 dirty 上。所以 dirty 包含了 read 中所有有效的数据,外加新数据。
  3. 旧 read 被丢弃:替换后,旧的 read map 对象会被垃圾回收(GC),但它里面的数据(entry)因为被新的 read(原来的 dirty)引用着,所以不会丢失。

直接替换 m.read 不会丢失数据,因为:

  1. 包含关系:在提升发生的那一刻,dirtyread超集(Superset)。dirty 拥有 read 的所有有效数据,加上 read 没有的新数据。
  2. 引用关系:它们共享底层的 entry 对象,所以值的更新是同步的。

详细分析:

我们需要看两个关键点:dirty 是怎么创建的 以及 数据是如何共享的

sync.Map 的核心数据结构是 entryreaddirty 的 map 定义都是 map[any]*entry

  • Key: 是具体的键值。
  • Value: 是一个指针 *entry

这意味着,如果一个 key 同时存在于 readdirty 中,它们指向的是堆内存中同一个 entry 结构体

  • 当你修改 read 中的某个 key 的 value 时,你实际上修改的是那个 entry
  • 因为 dirty 也指向这个 entry,所以 dirty 也能立即看到这个修改。

dirtynil 且需要写入新 key 时,会触发 dirtyLocked 方法。这个方法负责创建 dirty

// src/sync/map.go

func (m *Map) dirtyLocked() {
    if m.dirty != nil {
        return
    }

    read := m.loadReadOnly()
    // 1. 创建一个新的 map,大小等于 read
    m.dirty = make(map[any]*entry, len(read.m))
    
    // 2. 把 read 中所有“未被彻底删除”的数据,浅拷贝到 dirty 中
    for k, e := range read.m {
        if !e.tryExpungeLocked() {
            m.dirty[k] = e // 关键:这里复制的是指针!
        }
    }
}

所以只要 dirty 不为 nil,它就一定包含 read 中所有未被删除的 key。

假设我们有一个 Map,里面有 key “A”。

阶段 1: 初始状态 (dirty 为 nil)

  • read: { “A”: entryPtr1 }
  • dirty: nil

阶段 2: 发生 Store(“B”, 2) -> 触发 dirtyLocked

  • sync.Map 发现 dirty 是 nil,先执行 dirtyLocked
    • 创建 dirty map。
    • 把 “A” 从 read 复制到 dirty。此时 readdirty 都指向 entryPtr1
  • 然后把 “B” 写入 dirty
  • 状态
    • read: { “A”: entryPtr1 } (amended=true)
    • dirty: { “A”: entryPtr1, “B”: entryPtr2 }

阶段 3: 发生 Store(“A”, 3) -> 更新已有 Key

  • 直接修改 entryPtr1 的内容。
  • 因为 readdirty 都指向 entryPtr1,所以它们都看到了 “A” 变成了 3。

阶段 4: 发生 missLocked -> 提升 dirty 为 read

  • 执行 m.read.Store(&readOnly{m: m.dirty})
  • 新状态
    • read: { “A”: entryPtr1, “B”: entryPtr2 } (原本的 dirty)
    • dirty: nil
  • 旧 read: { “A”: entryPtr1 }。这个 map 对象被丢弃,等待 GC。