为什么有sync.Map
在日常编码中,map 是我们常用的数据结构之一,因为他能在o(1) 的时间复杂度内通过一个键值获取到对应的值, 这是非常高效的.然而,虽然 Go 提供了内置的 map,但它并不适用于多线程环境,因为在并发读写时可能会引发数据竞争,从而导致不安全的行为。
为了解决这一问题,Go 提供了 sync.Map,它在实现上做了特别设计,允许我们在多线程环境下安全、便捷地使用 map。接下来,我们就深入探索一下 sync.Map 的工作原理和优势。
sync.Map中的结构体
下面我们先来分析下sync.Map这个包下的一些结构体
1.Map
首先是map这个结构体, 包中最主要的结构体, 也是暴露在外给程序员调用的, 代码片段如下:
type Map struct {
mu Mutex
read atomic.Pointer[readOnly]
dirty map[any]*entry
misses int
}
接下来我将依次介绍每个变量的含义以及作用
- mu 是一把互斥锁, 用于保护 dirty map 和 misses 字段的访问。在多 goroutine 情况下,它防止了对这些共享资源的竞争。特别是在向 dirty map 写入数据时,需要持有 mu 锁,以确保写入操作的安全性。
- read atomic.Pointer 是go提供的一个支持安全并发读写操作的指针类型, 指针指向了一个自定义的结构体readonly (该结构体会在后面讲解), 可以简单理解为 *readOnly
- dirty 是一个标准的 Go map,用于保存那些已被修改但尚未同步到 read 的数据。dirty 中的数据可以理解为“脏数据”。这个 map 结构通常只有在写操作时才被访问,因此在并发读写操作中,需要持有 mu 锁来确保其操作的安全性。
- misses 一个记数器, 用于统计从 read 中查找失败(即未命中)次数。当某个 key 在 read 中未找到时,misses 计数器会增加。当 misses 的计数达到某个阈值后,Map 会将 dirty 中的数据同步到 read,然后重置 misses。
2.entry
// 插槽
type entry struct {
p atomic.Pointer[any]
}
在 sync.Map 中,entry 结构体充当了每个键值对的“插槽”,通过 atomic.Pointer 的原子性来避免加锁,从而提供了高效的并发读写能力。这种设计适合多读少写的场景,可以避免大多数锁操作所带来的性能开销。
p 是一个 atomic.Pointer 类型的指针,指向存储的任意类型的值(any 表示支持任何类型)。
3.readOnly
// 只读变量
type readOnly struct {
m map[any]*entry
amended bool
}
- m readOnly 结构体的主要字段,用于存储 sync.Map 中的键值对,供只读访问。m 中的数据是线程安全的,通常是稳定且不需要频繁更新的数据,这样可以实现快速、无锁的读取。
- amended 是一个布尔值,用来标记 readOnly 是否被修改过。 true代表Map的dirty字段有m中没有的key, 代表dirty已经被修改过, amended 标志来判断是否需要查找 dirty,可以减少不必要的加锁操作。
4.expunged
var expunged = new(any)
在 sync.Map 的实现中,expunged 是一个特殊的标志状态,用于标识一个 entry 已被“移除”或“清除”(通常理解为已删除)。该状态的引入是为了优化内存和性能管理,尤其是在 sync.Map 这种高并发结构中。
syncMap函数逐一解析
经过前面对结构体的介绍, 相信大家已经大概知道map包中每个结构体的作用, 现在我们就开始来解析每一个函数
内置函数(不对外暴露)
loadReadOnly
func (m *Map) loadReadOnly() readOnly {
if p := m.read.Load(); p != nil {
return *p
}
return readOnly{
}
}
该方法获取Map.read中的值, 如果获取为nil,则返回一个readOnly类型的空结构体
可以理解为对指针取值
missLocked
func (m *Map) missLocked() {
m.misses++
if m.misses < len(m.dirty) {
return
}
m.read.Store(&readOnly{
m: m.dirty})
m.dirty = nil
m.misses = 0
}
missLocked 方法用于在读取 sync.Map 时处理未命中的情况。未命中指的是在 read(快速读)中未找到某个 key,需要到 dirty(慢读)中查找。这个方法会记录未命中次数,当次数超过一定阈值后,将 dirty 提升为新的 read,并重置计数器,从而优化未来的读操作性能。
tryExpungeLocked
func (e *entry) tryExpungeLocked() (isExpunged bool) {
p := e.p.Load()
for p == nil {
if e.p.CompareAndSwap(nil, expunged) {
return true
}
p = e.p.Load()
}
return p == expunged
}
tryExpungeLocked 方法用于尝试将 entry 标记为已删除(expunged),仅在当前值为 nil 时执行。如果当前值不是 nil 或者已经标记为 expunged,则直接返回其状态。
dirtyLocked
func (m *Map) dirtyLocked() {
if m.dirty != nil {
return
}
read := m.loadReadOnly()
m.dirty = make(map[any]*entry, len(read.m))
for k, e := range read.m {
if !e.tryExpungeLocked() {
m.dirty[k] = e
}
}
}
dirtyLocked 方法用于确保 sync.Map 的 dirty 字段已初始化,并且从 read 中复制未被标记为删除的条目。它在写操作时调用,以便提供一个可写的 dirty 副本。
load
func (e *entry) load() (value any, ok bool) {
p := e.p.Load()
if p == nil || p == expunged {
return nil, false
}
return *p, true
}
该方法获取插槽中的值, 如果值等于nil或者该字段已经标记被删除, 则返回空
tryCompareAndSwap
func (e *entry) tryCompareAndSwap(old, new any) bool {
// 获取entry中的值, 如果p == nil 或者 p == expunged 则表示该entry已经被删除
// 若 *p != old 则表示当前值与期望的旧值不同,则不执行交换操作
p := e.p.Load()
if p

最低0.47元/天 解锁文章
35

被折叠的 条评论
为什么被折叠?



