【Go专家编程——常见数据结构的实现原理sync.Map】

1.sync.Map

sync.Map是一种并发安全的map,相较于原生map。sync.Map在并发读写时不会触发panic,它可以减轻程序员的负担。不用再小心翼翼地处理各种锁。

2.基础知识

  • sync.Map在读多写少的场景下的性能优于原生map+锁的机制。
  • sync.Map采用了两个冗余的数据结构结构(read map和dirty map)来实现读写分离。
  • sync.Map使用了锁,所以不能被拷贝
  • sync.Map在查询已存在的元素时,往往能够做到无锁访问。
  • sync.Map是针对特定场景的优化,并不能用于完全取代原生map。

3. 特性速览

3.1 用法

声明
sync.Map不需要使用make或字面量进行初始化
var m sync.Map,也就是说声明之后即可直接使用

增删改查

//增加(修改)
m.Store("Jim",80)//写入
m.Store("Kevin",85)
m.Store("Jim",90)//修改
//查询
score,_ := m.Load("Jim")
//删除
m.Delete("Jim")

sync.Map无法使用方括号[]来指定键值,所有接口均由方法提供。
sync.Map可存储任何类型的键值对,取出的元素类型为any(或interface{}),使用时需要使用类型断言。

其它接口

  • LoadOrStore(key,value any) (actual any, loaded bool)
    • 避免覆盖的Store
    • 如果指出的键存在,那么LoadOrStore将由actual返回相应的值。
    • 如果指出的键不存在,那么将这个键值对存入map
  • LoadAndDelete(key any) (value any, loaded bool)
    • 删除一个键,如果指定的键存在,则返回被删除的键值(类似于pop)
  • Range(f func(key, value any) bool)
    • sync.Map不能像原生map那样使用range遍历,提供了Range方法来实现遍历。Range会遍历每一个键值对并逐个调用回调函数,借此实现遍历。

3.2 使用要点

3.2.1 特定场景下可提升性能

sync.Map的内部实现采用了两个原生map来实现读写分离,数据读取并且能命中时能够提升性能,否则性能可能不如原生map,它仅在读多写少的场景下性能才有优势,并非在任意场景下的性能都优于原生map。

由于sync.Map会使用两个冗余的原生map,会使用更多的内存。无形中增加了GC的压力,在对内存大小或GC敏感的场景下应尽可能避免使用sync.Map

3.2.2 警惕类型安全风险

声明sync.Map时并不像声明原生map时那样指定了键和值的类型,事实上可以存储任意类型的数据,我们需要对返回的值进行类型断言,否者会在类型转换上触发panic风险。

3.2.3 不能拷贝和传递

由于结构中使用了锁sync.Mutex,因为锁是不能拷贝的(否则会造成死锁或触发panic)。所以sync.Map也不能拷贝

4. 实现原理

4.1 数据结构

4.1.1 sync.Map的数据结构

Go标准库中定义了sync.Map的数据结构

type Map struct{
	mu Mutex
	read atomic.Value	//read表,允许并发读
	dirty map[any]*entry	//dirty表,负载新数据写入
	misses int	// 记录read表查找miss的次数
}

dirty表仅仅是新数据的临时存放区,数据最终会同步会read表,同步的时机取决于misses,读取数据时会先查找read表,如果未找到则记录一次miss,待miss次数足够多(miss数等同于数据总数)时,则会触发数据同步。

sync.Map中高度互斥锁mu主要用于保护dirty表,同时在数据由dirty向read同步时起保护作用,避免多个同步操作并发执行。

4.1.2 readOnly的数据结构

前面看到sync.Map数据结构中read表的类型为atomic.Value,实际保存的数据则是名为readOnly的结构体

type readOnly struct{
	m	map[any]*entry
	// 标记dirty表中是否有不存在于read表中的数据
	amended bool
}
  • 当有新数据插入dirty时,amended标记为true。
  • 此时查询数据,若read表中不存在则会继续查询dirty表。
  • 当dirty表中的数据同步到read之后,amended为false
  • 此时查询数据,若read表中不存在则直接结束,省去加锁并查询dirty的时间花销
  • read表使用原子类型,主要是为了在数据同步(针对read的写操作)时不必阻塞其读取操作

4.1.3 entry的数据结构

entry是map中存放数据的曹巍,使用的是指针类型,好处是read表和dirty表可以实现共享内存,从而避免内存浪费。

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

4.2 增删改查

4.2.1 插入数据

流程:

  • 将数据插入dirty表
  • 将read表中的amended置为true,表面dirty表中有read表没有的数据

4.2.2 查找数据

流程:

  • 先读read表,若查找不到
  • 检查amended标志,为false则结束
  • 为true则查找dirty表,misses+1.当misses次数等于dirty表的大小事,触发转移

4.2.3 再次插入数据

流程:

  • 如果已存在
    • 则从read表中取出对应的值并使用原子操作直接完成修改
  • 若不存在
    • 则向dirty表中写数据,(同时把read表中的数据全部”复制过来“)

为什么需要把read表中的数据复制过来?

  • dirty表通过冗余read表中的数据从而维护一个全量数据,等到数据同步时,可以采用整表替换,不需要逐个遍历。
  • 同时read表中的数据可能会被删除,会存在一些空的entry槽位
  • 在这个”复制的过程“中其实也在剔除这些空的entry槽位,达到垃圾回收的效果
  • 所以当写多的场景时效率会低下

4.2.4 删除数据

流程:

  • 如果要删除的数据存在于read表中
    • 则直接把对应的entry的值置为nil
  • 如果数据仅存在于dirty表中
    • 则直接从dirty表中删除整个键值对,
  • 27
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值