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 了。
相关推荐
喵个咪11 小时前
GoWind 拆解:全链 ID 类型体系——int64 定版与前端字符串化契约
前端·后端·go
喵个咪12 小时前
GoWind CMS 拆解:发布审批——多级推进、逐级角色与全程留痕
前端·后端·go
喵个咪12 小时前
GoWind Flow 拆解:一个 Go 工作流引擎与它的双模式设计器
前端·后端·go
喵个咪12 小时前
GoWind CMS 拆解:搜索三板斧——高亮、联想、同义词
后端·搜索引擎·go
喵个咪12 小时前
GoWind 拆解:AI Agent 编排引擎——波次并行、子图递归与人工审批
人工智能·后端·go
喵个咪12 小时前
GoWind Admin 拆解:打印模板设计器——从画布到服务端 PDF 的完整打印链路
前端·后端·go
喵个咪13 小时前
GoWind Admin 拆解:BI 大屏设计器——schema 即 JSON、resolver 注入与三张快照表的口径谱系
大数据·后端·go
Warson_L1 天前
高性能架构设计
python·go
我不会起名字3221 天前
Redis 从 0 到 1(三十七·收官):36 篇浓缩成 1 张知识地图 + 50 项生产 Checklist
redis·go·知识体系·checklist·收官
Mz码不住1 天前
用 Wails 写一个跨平台进程管理器
go