Golang sync.Map 原理(两个map实现 读写分离、适用读多写少场景)

参考:

大家都知道go中的原生map是非线程安全的,多个协程并发读map常常会出现这样的问题 fatal error: concurrent map writes,所以一般为了使map能够做到线程安全,都会采取以下两种方式实现:

  • map + mutex (读多写少的场景下,锁的粒度太大存在效率问题:影响其他的元素操作)
  • sync.Map(减少加锁时间:读写分离,降低锁粒度,空间换时间,降低影响范围)

那么问题来了,sync.Map是如何做到线程安全的呢?一起来了解下~

sync.Map原理解析:

原理

sync.Map底层使用了两个原生map,一个叫read,仅用于读;一个叫dirty,用于在特定情况下存储最新写入的key-value数据:
在这里插入图片描述
read好比整个sync.Map的一个“高速缓存”,当goroutine从sync.Map中读数据时,sync.Map会首先查看read这个缓存层是否有用户需要的数据(key是否命中),如果有(key命中),则通过原子操作将数据读取并返回,这是sync.Map推荐的快路径(fast path),也是sync.Map的读性能极高的原因。

  • 操作:直接写入dirty(负责写的map)
  • 操作:先读read(负责读操作的map),没有再读dirty(负责写操作的map)
    在这里插入图片描述
    sync.Map 的实现原理可概括为:
  1. 通过 read 和 dirty 两个字段实现数据的读写分离,读的数据存在只读字段 read 上,将最新写入的数据则存在 dirty 字段上
  2. 读取时会先查询 read,不存在再查询 dirty,写入时则只写入 dirty
  3. 读取 read 并不需要加锁,而读或写 dirty 则需要加锁
  4. 另外有 misses 字段来统计 read 被穿透的次数(被穿透指需要读 dirty 的情况),超过一定次数则将 dirty 数据更新到 read 中(触发条件:misses=len(dirty))
    在这里插入图片描述

优缺点

  • 优点:Go官方所出;通过读写分离,降低锁时间来提高效率;
  • 缺点:不适用于大量写的场景,这样会导致 read map 读不到数据而进一步加锁读取,同时dirty map也会一直晋升为read map,整体性能较差,甚至没有单纯的 map+metux 高。
  • 适用场景:读多写少的场景。

可见,通过这种读写分离的设计,解决了并发场景下的写入安全,又使读取速度在大部分情况可以接近内建 map,非常适合读多写少的情况。

以上就是sync.Map的基本实现原理了,如果想要从源码角度去了解更多的底层实现细节,就继续往下学习~

如果有上述情况,那么会直接修改read中的值,而不是修改dirty中的值。所以不会出现read与dirty中数据不一致的情况。

补充:

:原本Go sync.Map在misses计数器达到阈值时,会同步dirty到read,以保证读操作获取最新数据。但如果写操作update更新了dirty中的对应key1,而随后的大量读请求都只读取到read中的旧数据,且misses不会增加,导致新数据无法同步到read。这时,如果读请求总是命中read,dirty中的新数据又无法更新到read,会不会存在read和write数据不一致的问题?

:在Store方法中,如果read中存在对应key,那么sync.Map会直接更新read中的值,而不是更新dirty。所以此时读read时,读到的就已经是最新的值。

func (m *Map) Store(key, value interface{
    }) {
    
    // 如果m.read存在这个key,并且没有被标记删除,则尝试更新。
    read, _ := m.read.Load().(readOnly)
    if e, ok := read.m[key]; ok && e.tryStore(&value) {
    
        return
    } 

sync.Map的 核心数据结构 及 源码解析

// sync.Map的核心数据结构
type Map struct {
   
	mu Mutex						// 对 dirty 加锁保护,线程安全
	read atomic.Value 				// readOnly 只读的 map,充当缓存层
	dirty map[interface{
   }]*entry 	// 负责写操作的 map,当misses = len(dirty)时,将其赋值给read
	misses int						// 未命中 read 时的累加计数,每次+1
}

// 上面read字段的数据结构
type readOnly struct {
   
    m  map[interface{
   }]*entry // 
    amended bool // Map.dirty的数据和这里read中 m 的数据不一样时,为true
}

// 上面m字段中的entry类型
type entry struct {
   
    // 可见value是个指针类型,虽然read和dirty存在冗余情况(amended=false),但是由于是指针类型,存储的空间应该不是问题
    p unsafe.Pointer // *interface{}
}

在 sync.Map 中常用的有以下方法:
- Load():读取指定 key 返回 value
- Delete(): 删除指定 key
- Store(): 存储(新增或修改)key-value

下面分别从这三种方法出发来理清底层代码逻辑:

Load()查询操作:
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值