sync.Map原理浅析

一、概述

sync.Map 常用于并发编程场景,通过原子操作保证读操作的线程安全性,同时利用不可变结构避免数据竞争。

  • 写操作:直接写入;
  • 读操作:先读 read 区,没有再读取 dirty 区;
graph TD A[写操作] --> B[dirty区] C[读操作] --> D[read区] --> E{get?} E -->|no|F[dirty区] E -->|yes|G[return] F --> G

二、核心数据结构

golang 复制代码
type Map struct {
    mu     Mutex
    read   atomic.Pointer[readOnly]
    dirty  map[any]*entry
    misses int
}
  • mu加锁作用。 一个 sync.Mutex 类型,保护后文的 dirty 字段。
  • read只读的数据。 这是一个 atomic.Pointer 类型,实际存储的是 readOnly 结构体。read 中的键值对可以无锁访问,适合读多写少的场景,能够显著提升读取性能。
  • dirty包含最新写入的数据。 它是一个普通的 map,存储的键值对可能包含 read 中没有的部分。当对 sync.Map 进行写操作(如插入、更新、删除)时,通常会先在 dirty 中进行操作。
  • misses计数作用。该计数器用于记录在 read 中查找键未命中的次数。当 misses 达到一定阈值时,会触发 dirty 提升为 read 的操作。
golang 复制代码
type readOnly struct {
    m       map[any]*entry
    amended bool 
}
  • m:它是一个普通的 map。
  • amended:一个 bool 类型的标志字段,Map.dirty 的数据和这里的 m 中的数据不一样的时候,为true。
golang 复制代码
type entry struct {
    p atomic.Pointer[any]
}
  • p:一个指针类型,指向实际存储的数据。

三、核心操作原理

sync.Map 是 Go 语言标准库中提供的一种并发安全的 map 类型。与普通的 map 不同,sync.Map 针对并发操作进行了优化,提供了一些原子操作来保证线程安全。其主要原理涉及到一些锁的优化和内部数据结构的设计。

3.1 数据读取流程

  1. 首先尝试从 read 中查找指定的键。如果找到,并且键对应的 entry 不为 nil,则直接返回该值。
  2. 如果在 read 中未找到,并且 read 的 amended 字段为 true,说明 dirty 中可能存在该键。此时会加锁,再次检查 read,若仍未找到,就从 dirty 中查找。
  3. 每次从 dirty 中查找后,会调用 missLocked 方法增加 misses 计数器的值。当 misses 达到 len(dirty) 时,会将 dirty 提升为 read,并清空 dirty。

3.2 数据删除流程

  1. 先尝试从 read 中查找该键。如果找到,将对应的 entry 标记为已删除。
  2. 如果在 read 中未找到,并且 read 的 amended 字段为 true,会加锁从 dirty 中删除该键。

3.3 数据增(改)流程

  1. 先尝试在 read 中查找该键,如果找到且 entry 未被标记为已删除,直接更新其值。
  2. 如果在 read 中未找到,或者 entry 已被标记为已删除,会加锁进行后续操作。
  3. 加锁后:
    1. 再次检查 read。若在 read 中找到且 entry 标记为已删除,将其加入 dirty 并更新值;
    2. 若在 dirty 中找到,直接更新值;
    3. 若都未找到,且 dirty 还未初始化,会将 read 中的所有键值对复制到 dirty,然后将新的键值对插入 dirty。

四、使用技术技巧

  1. 读写分离设计: 借助 read 和 dirty 两个映射来降低锁的使用频率,提升并发性能。
  2. 原子操作和 CAS(CompareAndSwap): sync.Map 使用了原子操作和 CAS 操作来执行读写操作,避免了整个map上的锁。这意味着在大多数情况下,读操作可以并发执行,而写操作只会锁定所在的那个段。
  3. 延迟删除 (Deferred Deletion): sync.Map 支持延迟删除(Lazy Deletion),即当一个 key 被删除时,并不会立即从底层的数据结构中移除,而是通过标记删除的方式,等待下一次 GC 执行删除。这样可以减小删除操作对并发读取的影响。
  4. 无锁访问: 在大多数情况下,sync.Map 会进行无锁访问,即读操作不需要锁。只有在写操作时,对应的段会被锁定,以确保写操作的原子性。

五、总结

  • 优点
    • Go官方所出,通过读写分离,降低锁时间来提高效率。
  • 缺点
    • 不适用于大量写的场景,这样会导致 read map 读不到数据而进一步加锁读取,同时 dirty map 也会一直晋升为 read map,整体性能较差,甚至没有单纯的 map + metux 高。
  • 适用场景
    • 适用于读多写少的场景。
  • 发散思考
    • 如果要实现一个类似的 Map,核心点就是把锁的粒度尽可能降低来提高运行速度。
    • 对一个大 map 进行 hash,其内部是 n 个小 map,根据 key 来 hash 确定在具体的那个小 map 中,这样加锁的粒度就变成 1/n 了。
相关推荐
郝同学的测开笔记13 小时前
云原生探索系列(十五):Go 语言通道
后端·云原生·go
夜寒花碎15 小时前
GO入门——Hello, World
后端·go
forever2316 小时前
jaeger组件部署
go
王中阳Go17 小时前
15~30K,3年以上golang开发经验
后端·面试·go
梦兮林夕20 小时前
06 文件上传从入门到实战:基于Gin的服务端实现(一)
后端·go·gin
孔令飞20 小时前
LLM 中的函数调用和工具是什么?
人工智能·云原生·go
江湖十年21 小时前
go-multierror: 更方便的处理你的错误列表
后端·面试·go
Pandaconda1 天前
【新人系列】Golang 入门(十三):结构体 - 下
后端·golang·go·方法·结构体·后端开发·值传递
Serverless社区2 天前
MCP 正当时:FunctionAI MCP 开发平台来了!
go
楽码2 天前
检查go语言变量内存结构
后端·go·计算机组成原理