GO并发安全字典sync.map(2)

并发安全字典如何做到尽量避免使用锁?
只读字典

sync.Map类型在内部使用了大量的原子操作来存取键和值,并使用了两个原生的map作为存储介质。
其中一个原生map被存在了sync.Map的read字段中,该字段是sync/atomic.Value类型的。这个原生字典可以被看作一个快照,它总会在条件满足时,去重新保存所属的sync.Map值中包含的所有键值对。可以理解为是只读字典
只读字典虽然不会增减其中的键,但却允许变更其中的键所对应的值。所以,它并不是传统意义上的快照,它的只读特性只是对于其中键的集合而言的。

type Map struct {
    mu Mutex
    read atomic.Value // readOnly
    dirty map[interface{}]*entry
    misses int
}

type readOnly struct {
    m       map[interface{}]*entry
    amended bool // true if the dirty map contains some key not in m.
}

由read字段的类型可知,sync.Map在替换只读字典的时候根本用不着锁。另外,这个只读字典在存储键值对的时候,还在值之上封装了一层。它先把值转换为了unsafe.Pointer类型的值,然后再把后者封装,并储存在其中的原生字典中。如此一来,在变更某个键所对应的值的时候,就也可以使用原子操作了。

脏字典

sync.Map中的另一个原生字典由它的dirty字段代表。它存储键值对的方式与read字段中的原生字典一致,它的键类型也是interface{},并且同样是把值先做转换和封装后再进行储存的。我们暂且把它称为脏字典。

其实到这里细心的同学对sync.map已经有了一个大题的了解。 看了两个关键类的定义。sync.map实现核心在于:
1.查找
两个字典,一个只读字典 一个脏字典,sync.Map在查找指定的键所对应的值的时候,总会先去只读字典中寻找,并不需要锁定互斥锁。只有当确定“只读字典中没有,但脏字典中可能会有这个键”的时候,它才会在锁的保护下去访问脏字典。
2.修改
sync.Map在存储键值对的时候,只要只读字典中已存有这个键,并且该键值对未被标记为“已删除”,就会把新值存到里面并直接返回,这种情况下也不需要用到锁。否则,它才会在锁的保护下把键值对存储到脏字典中。(这个时候,该键值对的“已删除”标记会被抹去。)
删除键值对,sync.Map会先去检查只读字典中是否有对应的键。如果没有,脏字典中可能有,那么它就会在锁的保护下,试图从脏字典中删掉该键值对。

查找修改的是都有atomic.Value保护,这样就减少了锁开销。

灵性点

只读字典和脏字典之间是会互相转换的。在脏字典中查找键值对次数足够多的时候,sync.Map会把脏字典直接作为只读字典,保存在它的read字段中,然后把代表脏字典的dirty字段的值置为nil。在这之后,一旦再有新的键值对存入,它就会依据只读字典去重建脏字典。
这个操作感觉很灵性,白话来讲就是想要的数据都在那个访问效率低地方,索性把这部分数据挪到效率高的地方让你开心的访问。不加锁,更快更安全。这个设计感觉很灵性。
可以看出,在读操作有很多但写操作却很少的情况下,并发安全字典的性能往往会更好。在几个写操作当中,新增键值对的操作对并发安全字典的性能影响是最大的,其次是删除操作,最后才是修改操作。

你可能感兴趣的:(GO并发安全字典sync.map(2))