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 了。
相关推荐
Mgx1 天前
深入理解 Windows 全局键盘钩子(Hook):拦截 Win 键的 Go 实现
go
王中阳Go1 天前
又整理了一场真实Golang面试复盘!全是高频坑+加分话术,面试遇到直接抄
后端·面试·go
itarttop1 天前
Go Error 全方位解析:原理、实践、扩展与封装
go
itarttop1 天前
Go Channel 深度指南:规范、避坑与开源实践
go
不爱笑的良田2 天前
从零开始的云原生之旅(十一):压测实战:验证弹性伸缩效果
云原生·容器·kubernetes·go·压力测试·k6
Java陈序员2 天前
代码检测器!一款专门揭露屎山代码的质量分析工具!
docker·go
豆浆Whisky2 天前
Go编译器优化秘籍:性能提升的黄金参数详解|Go语言进阶(16)
后端·go
不爱笑的良田2 天前
从零开始的云原生之旅(九):云原生的核心优势:自动弹性伸缩实战
云原生·容器·kubernetes·go
无限中终3 天前
ENERGY Designer:重构跨平台GUI开发的高效解决方案
重构·go·结对编程
shining4 天前
[Golang] 万字详解,深入剖析context
go