2-5.sync.Map的实现
概述
和 JAVA 中 concurrent hashmap 分段的设计思路不同,sync.Map 的主要思想是做读写分离,底层用了2个 map,read 和 dirty,将频繁变更的数据,和极少变更的数据分离。
极少变更的数据放到 read 里,读 read 无需加锁,更新 read 依靠 CAS 也不用加锁,性能极高。
新增的数据放到 dirty 里。这类数据的读写需要加锁。
读取数据时,先查 read,再查 dirty,当读 read cache miss 累计到一定次数时,说明该 key 读多写少了,将数据从 dirty 移入 read
实现
数据结构
// src/sync/map.go
type Map struct {
// 当涉及到脏数据(dirty)操作时候,需要使用这个锁
mu Mutex
// read 字段指向下面的 readOnly 结构,里面有个 map
// 【读/更新】不需要加锁,只需要通过 atomic 加载最新的指针即可
read atomic.Value
// dirty 包含部分 map 的键值对,如果操作需要 mutex 获取锁
// 【新增】键值时写入这个字段
dirty map[interface{}]*entry
// misses是一个计数器,用于记录read中没有的数据而在dirty中有的数据的数量。
// 也就是说如果read不包含这个数据,会从dirty中读取,并misses+1
// 当misses的数量等于dirty的长度,就会将dirty中的数据迁移到read中
misses int
}read的数据结构 readOnly:
entry
p有三种值:
nil: entry已被删除了,并且m.dirty为nil
expunged: entry已被删除了,并且m.dirty不为nil,但这个entry在于m.dirty中
其它:entry是一个正常的值
查找
先查找 read ,read 中没有再查找 dirty 、并增加 cache misses 计数
cache misses 数量累计至 dirty 的长度后,将 dirty 所有数据迁移至 read 中,并清空 dirty
新增和更新
如果 read 中已有 key,通过 CAS 来更新值
新增 key,或者更新 dirty 中的数据,需要加锁并写入 dirty。其中新增 key 时还会把 read 中的 kv 倒灌回 dirty
删除
和新增差不多,只不过不设置值了,而是调用底层 map 的 delete 方法
总结
仅建议用于如下两种场景:
写一次,读多次。比如程序初始化时加载配置。
多协程环境下,不同协程读写不同的key。
这两种场景下,相比于使用原生map + 全局锁的情况,sync.Map 能显著优化加锁带来的性能开销。
如果是读写穿插的情况,会频发触发 dirtyLocked 函数,里面会遍历 map 的 key 并进行迁移,效率非常差。
问题
既然读写 read 可以 CAS,那只要 read 不要 dirty 可不可以?
那同原生
map就没有区别了。sync.Map中,read仅用于读和更新已有key(这俩场景才能用CAS),无法用于新增key和触发扩容,后者的场景就需要用到dirty了。
Last updated