Golang源码探究 —— sync.Map

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

        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
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值