深入探索sync.Map(含源码解析)

为什么有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 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ambition!6

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值