Golang源码探究 —— sync.Map

本文详细解析了Golang中sync.Map的内部结构和实现原理,重点讨论了其如何通过两个映射处理并发读写,避免了普通map的并发问题。同时介绍了sync.Map在读写、删除操作中的逻辑,以及在不同场景下的性能优化。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

        Golang内置的map是不支持并发读写的,它在内部有检测机制,一旦发现并发读写,就会panic。如果需要并发读写map,有三种方案。1、使用map + Mutex 2、使用 map + RWMutex 3、使用sync.Map。前两者的效率在大部分情况下都不如官方提供的sync.Map。接下来来分析一下sync.Map是如何实现并发读写的。

 

1、sync.Map的结构

sync.Map的源码在sync/map.go中, skd版本:1.18

sync.Map的结构用图表示如下:

在这里插入图片描述

源码如下:

type Map struct {
    // 互斥锁
	mu Mutex


    /*
    	read中包含了一个可以并发访问的map,无需对mu加锁就可以访问,读写数据都会经过read map
    	它最终会包含这样一个结构:
    	type readOnly struct {
            m       map[interface{}]*entry
            amended bool     
        }
    */
	read atomic.Value // readOnly

    // dirty map 新增数据,走dirty map
	dirty map[interface{}]*entry

	// 未命中次数
	misses int
}

// readOnly is an immutable struct stored atomically in the Map.read field.
type readOnly struct {
	m       map[any]*entry
	amended bool      // 如果dirty中存在没有在m中的key时为true 
}

type entry struct {
	p unsafe.Pointer // *interface{}
}

        sync.Map结构体中包含了四个字段,其中包含了两个map[interface{}]*entry ,entry中包含了一个unsafe的Pointer,这个指针指向了真正的value值。

 

2、sync.Map读写

2.1 正常读写

Map的正常读写是走的read这个map,从read map中查找k-v。

在这里插入图片描述

2.2 追加问题

        假设我们要追加一个键值对"d":“go”,首先,因为不知道map中有没有这个key,因此需要先在read map中寻找,没有找到就会先对mu加锁,然后在下面的dirty map中追加。

在这里插入图片描述

        追加的时候,要创建一个entry,指针指向真正的值。然后要将amended置为true,表示read中的map已经不完整了,dirty map中有新追加的键值对。但是为什么要这样做?正常已存在的键值的读和修改走的都是上面的read map,而追加则是走的dirty map。因为上面的read map的读和写都不会涉及到map扩容的问题,而追加可能会导致map扩容。

在这里插入图片描述

2.3 追加后读写

        在追加后,我们要读出"d"的值,首先还是要走read这条线,read中找不到,但是amended为true,表示read中的map和dirty map不一致。因此要从dirty map中查找。读完后要把misses加1,表示一次要读的键在上面的map中未命中。随着多次的未命中,msses逐渐增加,当增加到misses == len(dirty)时,就会进行dirty提升。

在这里插入图片描述

2.4 dirty提升

        当misses与dirty map的长度相等时,就无法忍受了。因为多次在read中读取却未命中,还需要再走dirty map,这时候就会进行dirty map的提升。将上面的map干掉,将dirty提升上去,dirty变成新的read map,此时下面的dirty map为nil。后面如果追加的话,会重建dirty map。然后就会进入了初始的循环。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

 

3 sync.Map删除

  • sync.Map的删除相比于查询、修改、新增更麻烦。
  • 删除可以分为正常删除和追加后删除。
  • 提升后,被删的key还需要特殊处理。

3.1 正常删除

        在read map中根据key找到对应的entry,但是不是删除在map中对应的entry,而是将entry中的指针置为nil,这样就没有任何指针指向真正的值了,垃圾回收器就可以进行回收。

在这里插入图片描述

3.2 追加后删除

        假设"d"是我们刚追加上去的,read map和dirty map中的数据是不一致的。如果要删除"d",首先从read map中查找,没有找到,而且amended为true。对mu进行上锁,然后取dirty map中查找,找到后将entry中的指针置为nil。但是追加后删除涉及一个提升的问题,假设删除后,后面要将dirty map提升上来,那么四个键中的其中一个是指向nil的,如果要重建下面的dirty map,是否要重建这个nil的key呢?这个时候就不重建它,而是将指针指向expunged(删除了的意思)。将指针指向expunged就是为了提醒来访问的协程,这个键值对已经被删除了,如果要删除这个键值对的话,之间删除就行了,因为在下面已经没有了。

// expunged is an arbitrary pointer that marks entries which have been deleted
// from the dirty map.
var expunged = unsafe.Pointer(new(any))

在这里插入图片描述

在这里插入图片描述

 

4、源码分析

源码如下:

读取数据的逻辑:

/*
	从Map中读取数据
*/
func (m *Map) Load(key any) (value any, ok bool) {
    // 从atomic.Value中加载数据并断言为readOnly类型
	read, _ := m.read.Load().(readOnly)
    // 直接从read的map中读取
	e, ok := read.m[key]
    // 如果没有找到,并且read.amended为true,说明read map和dirty map中的数据不一致,因此需要再从dirty map中读取
	if !ok && read.amended {
        // 对mu上锁
		m.mu.Lock()
		// 防止当阻塞在lock上的时候,dirty map被提升了,因此需要重新获取一次read map
		read, _ = m.read.Load().(readOnly)
		e, ok = read.m[key]
        
		if !ok && read.amended {
            // 访问dirty map
			e, ok = m.dirty[key]
            // 无论是否找到了需要的键值,都记录一个miss
			m.missLocked()
		}
		m.mu.Unlock()
	}
	if !ok {
		return nil, false
	}
    
    // 从entry中获取数据
	return e.load()
}

// 从获取到的entry中的指针读取数据,如果指针为nil或者expunged说明要查找的key-val不存在
func (e *entry) load() (value any, ok bool) {
	p := atomic.LoadPointer(&e.p)
    // 如果p == expunged,说明已经被删除
	if p == nil || p == expunged {
		return nil, false
	}
	return *(*any)(p), true
}

// 记录未命中的次数,如果次数达到dirty map的长度时,提升dirty map
func (m *Map) missLocked() {
	m.misses++
	if m.misses < len(m.dirty) {
		return
	}
    // 如果mmisses == len(m.dirty) 就要提升dirty
	m.read.Store(readOnly{m: m.dirty})
	m.dirty = nil
	m.misses = 0
}

修改或新增数据逻辑:

/*
	修改或新增键值对
*/
func (m *Map) Store(key, value any) {
    // 获取read
	read, _ := m.read.Load().(readOnly)
    // 先从read map中查询是否存在,如果存在,就是修改操作,调用tryStore修改read map中的数据
	if e, ok := read.m[key]; ok && e.tryStore(&value) {
		return
	}
	
    // 要修改的数据可能在dirty map中,属于2.3的情况 或者是不在dirty map中,那么就是新增k-v操作
    
    // 先上锁
	m.mu.Lock()
    // 再次查询read map防止阻塞在lock期间,dirty map被提升
	read, _ = m.read.Load().(readOnly)
    // ok == true说明dirty map已经被提升,而且要修改的数据在原来的dirty map中
	if e, ok := read.m[key]; ok {
		if e.unexpungeLocked() {
            // entry被删除了
			m.dirty[key] = e
		}
		e.storeLocked(&value)
    // 查找dirty map,如果找到,就修改dirty map中的数据    
	} else if e, ok := m.dirty[key]; ok {
		e.storeLocked(&value)
    // 追加数据
	} else {
        // 
		if !read.amended {
            // 第一次新增,此时amended为false,可能是dirty提升后第一次新增,此时需要重建dirty map
			m.dirtyLocked()
            // 修改amended为true
			m.read.Store(readOnly{m: read.m, amended: true})
		}
        // 新增数据
		m.dirty[key] = newEntry(value)
	}
	m.mu.Unlock()
}

/*
	修改read map中的值
*/
func (e *entry) tryStore(i *any) bool {
	for {
		p := atomic.LoadPointer(&e.p)
        // 说明已经被删除
		if p == expunged {
			return false
		}
        // 原子操作,修改值
		if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
			return true
		}
	}
}

/*
	第一次新增,dirty map可能被提升了,如果被提升,那么dirty map为nil,需要重建
*/
func (m *Map) dirtyLocked() {
	if m.dirty != nil {
		return
	}
	// 重建dirty map
	read, _ := m.read.Load().(readOnly)
	m.dirty = make(map[any]*entry, len(read.m))
	for k, e := range read.m {
        // 不会添加expunged的k
		if !e.tryExpungeLocked() {
			m.dirty[k] = e
		}
	}
}

删除逻辑:

/*
	删除键对应的值
*/
func (m *Map) Delete(key any) {
	m.LoadAndDelete(key)
}

/*
	删除键对应的值并返回原值
*/
func (m *Map) LoadAndDelete(key any) (value any, loaded bool) {
	read, _ := m.read.Load().(readOnly)
    // 判断要删除的key是否在read map中
	e, ok := read.m[key]
    // 不在read map中,并且read map和dirty map中的数据不一致
	if !ok && read.amended {
		m.mu.Lock()
        // 同样的,防止在阻塞期间,dirty map被提升了
		read, _ = m.read.Load().(readOnly)
		e, ok = read.m[key]
		if !ok && read.amended {
            // 在dirty map中
			e, ok = m.dirty[key]
            // 直接删除
			delete(m.dirty, key)
			// 记录未命中次数
			m.missLocked()
		}
		m.mu.Unlock()
	}
    // 并没有从read map中删除val,而是将entry中的指针置为nil
	if ok {
		return e.delete()
	}
	return nil, false
}

/*
	将val从entry中删除
*/
func (e *entry) delete() (value any, ok bool) {
	for {
		p := atomic.LoadPointer(&e.p)
        // p == nil说明已经被删除
        // p == expunged 说明原来在dirty map中被删除了,并且原来的dirty map被提升为了read map
		if p == nil || p == expunged {
			return nil, false
		}
        // 将entry中的指针置为nil
		if atomic.CompareAndSwapPointer(&e.p, p, nil) {
			return *(*any)(p), true
		}
	}
}

 

5、总结

  • 普通map在扩容时会有并发问题
  • sync.Map使用了两个map,分离了扩容问题
  • 不会引发map扩容的操作(查、改)使用read map
  • 可能引发扩容的操作(新增)使用dirty map
  • 读写操作都发送在read map,虽然普通map有并发读写问题,遇到并发读写时会panic,而且读写read map时没有加锁。是因为read map在读写时没有真正的对普通map进行写入,而是只有读取操作,读取entry,然后再读写entry中的指针,使用的是原子操作,因此不会产生并发读写map的问题
  • sync.Map在读多、写多、追加少的情况下性能比较好
<think>嗯,用户问的是Go语言中的sync.Once怎么使用。我需要先回忆一下sync.Once的基本概念和用途。记得sync.Once是用来确保某个操作只执行一次,不管有多少个goroutine调用它。这在单例模式或者初始化配置时很有用。 首先,我应该解释一下sync.Once的作用,让用户明白为什么需要这个结构。然后,可能需要举一个例子,说明如何声明和使用它。比如,用Do方法包裹初始化函数,确保只运行一次。 然后,用户可能想知道具体的用法步骤,比如声明一个sync.Once变量,然后在需要的地方调用Do方法,传入要执行的函数。需要注意的是,传入的函数不能带参数,所以如果有参数的话,可能需要用闭包或者封装一下。 接下来,应该举一个实际的代码示例,展示如何在实际中使用sync.Once。比如初始化一个数据库连接或者配置文件,确保多个goroutine同时调用时,初始化只执行一次。这时候,可能需要用一些变量来存储初始化后的结果,比如全局变量或者结构体中的字段。 另外,需要提醒用户一些注意事项,比如传入的函数如果有不同的逻辑,可能会导致多次执行,因为sync.Once是根据实例来判断的。所以每个不同的操作需要单独的sync.Once实例。另外,错误处理的问题,如果在Do中的函数执行失败,再次调用Do时不会重试,所以需要内部处理错误,或者记录错误状态。 可能还需要提到sync.Once的内部实现原理,比如使用原子操作和互斥锁来保证并发安全,这样用户能更好地理解其高效性。但不需要太深入,点到为止即可。 用户可能会有疑问,如果有多个不同的初始化函数,应该怎么处理。这时候需要说明每个初始化函数都需要自己的sync.Once实例。比如,初始化配置和初始化数据库连接应该分别使用不同的sync.Once变量。 另外,错误处理是一个重要的点,如果在Do里面的函数返回了错误,用户可能想知道怎么处理这种情况。这时候应该建议在函数内部处理错误,或者设置一个全局的错误变量,在调用Do之后检查是否有错误发生。 总结一下,回答的结构应该是先介绍用途,然后基本用法,接着代码示例,再是注意事项,最后可能加上内部原理的简要说明。这样用户能够逐步理解并正确使用sync.Once。</think>### 一、sync.Once 的作用 `sync.Once` 是 Go 语言标准库中用于 **保证某个操作仅执行一次** 的并发安全工具。典型场景包括: - 单例模式(如数据库连接池初始化) - 延迟初始化(按需加载配置) - 避免重复计算 --- ### 二、基础用法 #### 1. 声明与调用 ```go var once sync.Once // 需要保证只执行一次的函数 initFunc := func() { fmt.Println("This runs only once") } // 在多个 goroutine 中调用 once.Do(initFunc) ``` #### 2. 完整示例 ```go package main import ( "fmt" "sync" ) var ( config map[string]string once sync.Once ) func loadConfig() { fmt.Println("Loading config...") config = map[string]string{"env": "prod", "timeout": "30s"} } func GetConfig() map[string]string { once.Do(loadConfig) // 并发安全初始化 return config } func main() { // 启动多个 goroutine 测试 var wg sync.WaitGroup for i := 0; i < 5; i++ { wg.Add(1) go func() { defer wg.Done() fmt.Println(GetConfig()) // 只有第一次会触发 loadConfig() }() } wg.Wait() } ``` 输出: ``` Loading config... map[env:prod timeout:30s] map[env:prod timeout:30s] map[env:prod timeout:30s] map[env:prod timeout:30s] map[env:prod timeout:30s] ``` --- ### 三、关键注意事项 #### 1. 函数无参数 通过闭包解决参数传递问题: ```go var ( dbConn string dbOnce sync.Once ) func InitDB(host string) { dbOnce.Do(func() { // 使用闭包传递参数 dbConn = fmt.Sprintf("connect to %s", host) }) } ``` #### 2. 错误处理 若初始化可能出错,建议内部记录错误状态: ```go var ( cache map[string]int once sync.Once initErr error ) func initCache() { defer func() { if r := recover(); r != nil { initErr = fmt.Errorf("init failed: %v", r) } }() // 可能出错的操作 cache = make(map[string]int) cache["key"] = 1 / 0 // 模拟 panic } func GetCache() (map[string]int, error) { once.Do(initCache) return cache, initErr } ``` #### 3. 不同操作需独立实例 ```go var ( onceA sync.Once onceB sync.Once ) func InitA() { onceA.Do(/* ... */) } func InitB() { onceB.Do(/* ... */) } // 需使用不同的 Once 实例 ``` --- ### 四、实现原理 `sync.Once` 通过 `uint32` 原子标志位 + 互斥锁实现: 1. 原子操作检查标志位 `done` 2. 首次执行时加锁运行函数 3. 后续调用直接返回 源码关键片段: ```go type Once struct { done uint32 m Mutex } func (o *Once) Do(f func()) { if atomic.LoadUint32(&o.done) == 0 { o.doSlow(f) } } func (o *Once) doSlow(f func()) { o.m.Lock() defer o.m.Unlock() if o.done == 0 { defer atomic.StoreUint32(&o.done, 1) f() } } ``` --- ### 五、典型应用场景 1. 全局配置加载 2. 数据库连接池初始化 3. 文件系统单次检查 4. 日志处理器设置 --- ### 六、性能建议 - 高频调用场景下性能优异(原子操作消耗极小) - 避免在 `Do` 中执行耗时操作(会阻塞后续调用) - 对需要重置的场景,建议重新创建实例而非修改 `done` 标志
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值