1.map并发问题
1.1map线程安全
map不是线程安全的,在查找、赋值、遍历、删除的过程中都会先检查写标志,一旦发现写标志位等于1,则直接panic。赋值和删除函数在检查完写标志位是复位状态(等于0)之后,先将写标志位置位(置为1)才会进行赋值和删除的操作。
为什么会panic:并发读写的情况下,map里面的数据会被写乱,会对gc造成问题,还有扩容时候出现新旧桶数据不一致导致的并发问题。
所以在并发读写操作map时,需要加锁,但是会降低很大的性能,所以这个时候sync.map横空出世!
1.2sync.map
- 使用场景:适用于读多写少的场景。对于写多的场景,会导致 read map 缓存失效,需要加锁,导致冲突变多;而且由于未命中 read map 次数过多,导致 dirty map 提升为 read map,这是一个 O(N) 的操作,会进一步降低性能。
2.数据结构
type Map struct {
mu Mutex
read atomic.Value // readOnly
dirty map[interface{}]*entry
misses int
}
// Map.read 属性实际存储的是 readOnly。
type readOnly struct {
m map[interface{}]*entry
amended bool
}
mu:互斥锁,保护read和dirty字段
read:只读数据,可以并发的读—atomic.Value类型,如果需要更新read就需要加锁保护。
amended字段:标记read和dirty的数据是否一致。
dirty:读写数据,是一个非线程安全的原始map,包含新写入的key和read中未删除的key。
misses:每次从read中读取失败,就会misses+1,当加到一定的阈值就会将dirty提升为read。
2.sync.map的整体结构:
1.结构如下所示:
下面详细介绍一下在sync.map中的增删改查操作:
2.1.查询具体流程
sync.Map 的两个 map,当从 sync.Map 类型中读取数据时,其会先查看 read 中是否包含所需的元素:
若有,则通过 atomic 原子操作读取数据并返回。
若无,则会判断 read.readOnly 中的 amended 属性,他会告诉程序 dirty 是否包含 read.readOnly.m 中没有的数据;因此若存在,也就是 amended 为 true,将会进一步到 dirty 中查找数据。
2.2.sync.map添加元素
例如sync.map追加—例增加“d”:”D”
map会先去m中查询,如果m中不存在相应的d键值对,就加锁 mu,去dirty中增加新的键值对
增加方法:在dirty中加入d,值为指向一个结构体的指针,pointer为万能指针,结果为“D”,并且将amended置为TRUE。
2.3.sync.map追加后读写
1.追加后读写,查询先去m中查找,如果没找到并且amended为true,则去dirty中查找,并且将misses+1。当增加到misses==length(dirty)时候,实现dirty提升。
2.dirty提升,misses=0,amended=false,底下的dirty表初始为nil,如果有追加操作在像里面赋值,重建dirty。
2.4.sync.map删除问题
因为涉及到两个表的并发性和数据一致性问题,还有dirty表提升的问题,删除问题相比而言比较复杂。先介绍一下p指针,就是上图中的pointer万能指针。
1.当 p == nil
时,说明这个键值对已被删除,并且 m.dirty == nil,或 m.dirty[k] 指向该 entry。
2.当 p == expunged
时,说明这条键值对已被删除,并且 m.dirty != nil,且 m.dirty 中没有这个 key。
其他情况,p 指向一个正常的值,表示实际 interface{}
的地址,并且被记录在 m.read.m[key] 中。如果这时 m.dirty 不为 nil,那么它也被记录在 m.dirty[key] 中。两者实际上指向的是同一个值。
当删除 key 时,并不实际删除。一个 entry 可以通过原子地(CAS 操作)设置 p 为 nil 被删除。如果之后创建 m.dirty,nil 又会被原子地设置为 expunged,且不会拷贝到 dirty 中。
如果 p 不为 expunged,和 entry 相关联的这个 value 可以被原子地更新;如果 p == expunged
,那么仅当它初次被设置到 m.dirty 之后,才可以被更新。
Q:为什么要设置nil和expunged两种状态:
expunged是在dirty 时增加并且删除了数据,当dirty提升为map的时候,新的dirty中并不存在该数据,为了保证删除操作的正确性和实现读写与追加分离设置了这个状态。
3.总结
总结:sync.map使用了两个map,分离了扩容问题,实现map的并发性。
在不会引发扩容问题的操作(查改)使用readmap
在会引发扩容问题的操作(新增)使用dirtymap