sync Map
sync.Map 设计非常精巧,但也很复杂。它的核心目标是让 读取操作尽可能快,甚至无需加锁。
1. 核心思想⚓
它内部维护两个 map:一个用于快速、无锁读取的 read map,和一个需要加锁才能访问的 dirty map。
readmap: 这是一个atomic.Value,里面存放一个readOnly结构体。这个结构体包装了一个 Go 内置的map。因为是原子值,所以可以被多个 goroutine 并发地、无锁地读取。它存储了 map 的一个“快照”。dirtymap: 这是一个普通的 Gomap,由一个sync.Mutex互斥锁保护。它包含了readmap 的所有数据,以及任何新增或修改的数据。可以把它看作是“最新最全”的数据集。
2. 数据结构 (简化)⚓
type Map struct {
mu sync.Mutex
read atomic.Value // 存储 readOnly 结构体
dirty map[any]*entry
misses int // 在 read 中未命中的次数
}
type readOnly struct {
m map[any]*entry
amended bool // dirty map 是否包含了 read map 中没有的新 key
}
type entry struct {
p unsafe.Pointer // *interface{}
}
var expunged = unsafe.Pointer(new(interface{})) // 唯一哨兵
read:只读快照(类型类似readOnly),逻辑不可变。dirty:可变表(map[any]*entry),承载所有写入与未命中补充。mu:全局互斥锁,仅在需要访问或更新dirty进行提升时持有。misses:读未命中的计数,用来判断何时触发提升(把dirty合并进read)。entry: 原子状态槽,对于 dirty 中存在的每个 entry,read 和 dirty 是共用的p:一个unsafe.Pointer指向实际的interface{}值。- 三态语义:
- 有效值:
p指向一个非 nil 的interface{}。 - nil:逻辑删除,值不可见,但可“复活”(重新写值)——取决于是否被标记为 expunged。
- expunged:特殊哨兵指针,表示彻底删除,用于阻止从
read直接复活;若要复活必须在dirty新建 entry。
- 有效值:
3. 操作流程⚓
-
读取 (Load)
- 快速路径: 直接从
read这个atomic.Value中加载readOnlymap。如果找到了 key 并且它没有被标记为“已删除”,直接返回。这个过程完全无锁,非常快。 - 慢速路径: 如果在
read中没找到,就需要加锁 (mu.Lock()) 访问dirtymap。 - 加锁后,再次检查
read(双重检查),因为可能在你等待锁的时候,其他 goroutine 已经把dirtymap 提升为了新的readmap。 - 如果仍然没有,就从
dirtymap 中查找。 - 同时,
misses计数器会递增。当misses数量达到len(dirty)时,会触发一次“提升”操作:将dirtymap 变成新的readmap,并清空dirty。这是一种成本分摊机制。
- 快速路径: 直接从
-
写入 (Store)
- 首先检查 key 是否存在于
readmap 中。如果存在,尝试通过原子操作(CAS)直接更新 entry 的值。这也是一个快速路径。 - 如果 key 不在
readmap 中,或者原子更新失败,就进入慢速路径。 - 加锁 (
mu.Lock()),将新的 key-value 写入dirtymap。如果dirtymap 为 nil,则需要先将readmap 的内容完整复制过来。
- 首先检查 key 是否存在于
-
删除 (Delete) 删除操作是“软删除”。它只是在
readmap 中通过原子操作将 entry 的值标记为一个特殊的“已删除”状态(expunged),而不是真正地从 map 中移除。真正的物理删除发生在dirtymap 提升为readmap 的过程中。
4. 优点⚓
- 极快的读取性能: 在 key 已存在且不被频繁修改的情况下,读取操作几乎等同于一次原子指针读取,性能极高。
5. 缺点⚓
- 写入性能波动大: 当
dirtymap 被提升为readmap 时,需要复制整个 map,这是一个耗时操作,会造成性能的突然抖动(Latency Spike)。 - 高内存占用: 在某些情况下,
dirtymap 可能是readmap 的一个完整副本,导致双倍的内存占用。 - 代码逻辑复杂:
read、dirty、misses、amended等状态使得内部逻辑非常复杂,难以理解和维护。 Range遍历性能差:Range操作需要加锁,并且要合并read和dirty两部分数据,效率不高。