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表中删除整个键值对,