对于并发读写map的情况下,map的数据会被写乱造成panic。Go语言原生map并不是线程安全的,因此对它进行并发读写操作时需要加锁。但是当操作频繁且要求性能的情况下,锁的优化已经无法满足业务需求,考虑到互联网应用通常是读多写少的场景,Golang的标准库提供了一个特殊的并发安全的map实现,为了与原生map区分,人们习惯性称为sync.map。
一、sync.map与map的区别
1.并发安全
sync.Map内部使用了锁和其他同步原语来保证并发访问的安全性
2.无需初始化
sync.Map的零值是为空的map不是nil,因此不需要使用make函数初始化,可以直接声明后使用。
3.特殊的API
sync.Map提供特定的方法如Load、Store等,与内置map的语法不同
二、基础增删改查,遍历及其他方法
增删改查+遍历
func text(){
var mymap sync.Map
/*
增(或修改)
*/
mymap.Store("apple",10) //增加键为apple值为10的数据
mymap.Store("apple",5 ) //修改键为apple的值为5
/*
查
*/
num, _ := mymap.Load("apple")
fmt.Printf("apple num=%d\n",num.(int))//输出5
/*
删
*/
mymap.Delete("apple")//删除apple及对应的值
/*
遍历
*/
mymap.Range(func(key,value interface{})bool{
fmt.Printf("%s:%d\n",key,value)
return true
})
}
其他方法
(1)LoadOrStore
func main(){
var mymap sync.Map
sub1, ok1 := mymap.LoadOrStore("key1","value1")
fmt.Println(sub1,ok1)//输出value1,false
sub2,ok2 := mymap.LoadOrStore("key1","new_value1")
fmt.Println(sub2,ok2)//输出 value1,true
}
通过示例,在sync.Map中使用LoadOrStore方法添加键值对时,若key存在,则返回旧的值和true;若key不存在,则会添加一个新的键值对并返回相应的值和false
(2)LoadAndDelete
若被删除的键存在,则返回被删除的键值和true;若不存在则返回false表示键不存在
三、sync.Map的适用场景
sync.Map通过使用read和dirty两个map来进行读写分离,降低锁时间来提高效率。
sync的使用场景
1.key的集合基本不变,但是value会并发更新:在这种场景下,sync.Map通过将热点数据分离出来,减少锁的争用,提高了性能。
2.k-v对的添加和删除操作比较少,但是读的操作非常频繁:sync.Map在读取操作山做了优化,读操作通常无需加锁,这大大提高了并发读的性能。
sync的适用场景
1.并发环境下的缓存系统:缓存项被频繁读取,但更新和删除操作比较少。
2.长运行的监听器列表:监听器被添加后很少改变,但可能会被频繁触发。
3.全局状态和配置:全局配置可能会在程序启动时被设置,之后只会被读取。
四、sync.Map设计原理及源码分析
1.核心思想
尽可能无锁化:要实现并发安全,很难做到无锁化。但是为了提高性能,应该尽可能使用原子操作,最大化减少锁的使用。
读写分离:读写分离式针对读多写少场景的常用手段,面对读多写少的场景能够提供高性能的访问。
2.数据结构分析
sync.Map的结构体定义
// sync/map.go
type Map struct {
mu Mutex // 互斥锁,用于保护dirty字段和misses字段。
read atomic.Value // readOnly, 一个atomic.Value类型的字段,存储了一个readOnly结构体,用于存储只读数据。
dirty map[interface{}]*entry // 一个map,存储了所有可写的键值对。
misses int // 一个计数器,记录了从read读取失败的次数,用于触发将数据从dirty迁移到read的决策。
}
type readOnly struct {
m map[interface{}]*entry // 实际存储键值对的map。
amended bool // 标记位,如果dirty中有read中没有的键,那么为true
}
sync.Map使用两个原生的map(本质上是map[interface{}]*entry)来作为数据的存储空间分别是:
- read:只读字典, 使用atomic.Value来承载,保证原子性和高性能, 但不保证数据的完整性(不保证拥有全部的Key),相当于某个时间的Key-Value对的快照。但如果需要更新
read
,则需要加锁保护。对于 read 中存储的 entry 字段,可能会被并发地 CAS 更新。但是如果要更新一个之前已被删除的 entry,则需要先将其状态从 expunged 改为 nil,再拷贝到 dirty 中,然后再更新。 - dirty:脏字典, 用互斥锁Map.mu来保护,保证了并发安全。如果m.dirty!=nil, 则dirty包含了所有的Key-Value对。当新增一个Key时,会先存放在dirty中,然后等满足一定条件后再同步给read。如果dirty为 nil,那么下一次写入时,会新建一个新的dirty,这个初始的dirty是read 的一个拷贝,但除掉了其中已被删除的 key。
entry是对实际数据的封装
type entry struct{
p unsafe.Pointer //*interface{} 一个指向实际数据的指针
}
var EXPUNGED = unsafe.Pointer(new(any))
entry中的p有三种情况:
1.e.p==nil:
entry已经被标记删除,不过因为还未进行read=>dirty的同步,因此dirty中可能还存在该entry
2.e.p == expunged
entry已经被标记删除,且已经完成read=>dirty同步,因而不属于dirty,仅仅属于read,下一次dirty=>read升级,会被彻底清理。延迟删除的思想。
3.e.p为正常值
键值对存在,存在于m.read.m中,如果m.dirty!=nil 则存在于m.dirty
总结
1.sync.Map是线程安全的,读取,插入,删除也都保持着常数级的时间复杂度。
2.通过读写分离,降低锁时间来提高效率,适用于读多写少的场景。
3.Range 操作需要提供一个函数,参数 是 k,v ,返回值是一个布尔值:f func(key,value interface{}) bool
4.调用 Load 或 LoadOrStore 函数时,如果在 read 中没有找到 key,则会将 misses 值原子地增加 1,当 misses 增加到和 dirty 的长度相等时,会将 dirty 提升为 read。以期减少“读 miss”。
5.新写入的 key 会保存到 dirty 中,如果这时 dirty 为 nil,就会先新创建一个 dirty,并将 read 中未被删除的元素拷贝到 dirty。
6.当 dirty 为 nil 的时候,read 就代表 map 所有的数据;当 dirty 不为 nil 的时候,dirty 才代表 map 所有的数据。