一、概述
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 数据读取流程
- 首先尝试从 read 中查找指定的键。如果找到,并且键对应的 entry 不为 nil,则直接返回该值。
- 如果在 read 中未找到,并且 read 的 amended 字段为 true,说明 dirty 中可能存在该键。此时会加锁,再次检查 read,若仍未找到,就从 dirty 中查找。
- 每次从 dirty 中查找后,会调用 missLocked 方法增加 misses 计数器的值。当 misses 达到 len(dirty) 时,会将 dirty 提升为 read,并清空 dirty。
3.2 数据删除流程
- 先尝试从 read 中查找该键。如果找到,将对应的 entry 标记为已删除。
- 如果在 read 中未找到,并且 read 的 amended 字段为 true,会加锁从 dirty 中删除该键。
3.3 数据增(改)流程
- 先尝试在 read 中查找该键,如果找到且 entry 未被标记为已删除,直接更新其值。
- 如果在 read 中未找到,或者 entry 已被标记为已删除,会加锁进行后续操作。
- 加锁后:
- 再次检查 read。若在 read 中找到且 entry 标记为已删除,将其加入 dirty 并更新值;
- 若在 dirty 中找到,直接更新值;
- 若都未找到,且 dirty 还未初始化,会将 read 中的所有键值对复制到 dirty,然后将新的键值对插入 dirty。
四、使用技术技巧
- 读写分离设计: 借助 read 和 dirty 两个映射来降低锁的使用频率,提升并发性能。
- 原子操作和 CAS(CompareAndSwap): sync.Map 使用了原子操作和 CAS 操作来执行读写操作,避免了整个map上的锁。这意味着在大多数情况下,读操作可以并发执行,而写操作只会锁定所在的那个段。
- 延迟删除 (Deferred Deletion): sync.Map 支持延迟删除(Lazy Deletion),即当一个 key 被删除时,并不会立即从底层的数据结构中移除,而是通过标记删除的方式,等待下一次 GC 执行删除。这样可以减小删除操作对并发读取的影响。
- 无锁访问: 在大多数情况下,sync.Map 会进行无锁访问,即读操作不需要锁。只有在写操作时,对应的段会被锁定,以确保写操作的原子性。
五、总结
- 优点
- Go官方所出,通过读写分离,降低锁时间来提高效率。
- 缺点
- 不适用于大量写的场景,这样会导致 read map 读不到数据而进一步加锁读取,同时 dirty map 也会一直晋升为 read map,整体性能较差,甚至没有单纯的 map + metux 高。
- 适用场景
- 适用于读多写少的场景。
- 发散思考
- 如果要实现一个类似的 Map,核心点就是把锁的粒度尽可能降低来提高运行速度。
- 对一个大 map 进行 hash,其内部是 n 个小 map,根据 key 来 hash 确定在具体的那个小 map 中,这样加锁的粒度就变成 1/n 了。