Sirius
Sirius

目录

Golang sync.Map

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

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

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

为了实现这个目标,它牺牲了写操作的性能,并使用了比普通 map 更复杂的内部结构。

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),这是一个逻辑删除,而不是物理删除。

Load 操作在 read 中未命中,且 misses 计数器增长到等于 dirty map 的大小时,会触发这个过程:

  1. 加锁 mu

  2. dirty map 中的所有数据(包括 read 中已有的但被更新的数据)合并到一个新的 map 中,这个新 map 将成为新的 readOnly.m

  3. 通过原子操作,将 sync.Mapread 指针指向这个包含全新数据的 readOnly 结构。

  4. dirty map 清空(或设为 nil),并将 misses 计数器重置为0。

  5. 解锁 mu

特性 手动加锁 (map + sync.RWMutex) sync.Map
核心机制 单一锁(读写锁)保护整个 map 两个 map(read/dirty),读大多无锁,写加锁
最佳使用场景 写多读少,或读写均衡的场景;或map创建后不经常修改的场景 读多写少,特别是“一次写入,多次读取”的场景 (如缓存)
性能特点 读和写操作都会有锁的开销,高并发读写时锁竞争激烈 键稳定存在时,读操作极快(接近原子操作);写操作相对较慢
锁竞争与扩展性 整个 map 共享一把锁,高并发下所有操作都可能互相阻塞,扩展性受限 读操作无锁竞争,写操作有锁,但读写分离,显著降低了读操作的竞争,在高并发读场景下扩展性好
API 与易用性 开发者需手动管理锁,但 API 就是普通 map 的 API 封装好的 API (Load, Store, Delete, Range),无需手动管锁,但 API 功能受限(如无法直接获取长度)
类型安全 编译时类型安全map[string]int 运行时类型断言key, value 都是 any 类型)