和 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:
// readOnly is an immutable struct stored atomically in the Map.read field.
type readOnly struct {
// m 包含所有只读数据,不会进行任何的数据增加和删除操作
// 但是可以修改 entry 的指针(即更新 value),因为这个不会导致map的元素移动
m map[interface{}]*entry
// 标志位,如果为true则表明当前read只读map的数据不完整,dirty map中包含部分数据
amended bool // true if the dirty map contains some key not in m.
}
entry
type entry struct {
p unsafe.Pointer // *interface{}
}
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
// src/sync/map.go
// Load returns the value stored in the map for a key, or nil if no
// value is present.
// The ok result indicates whether value was found in the map.
func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
// 先从只读 read 的 map 中查找,这时不需要加锁
read, _ := m.read.Load().(readOnly)
e, ok := read.m[key]
// 如果没有找到,并且 read.amended 为 true,说明 dirty 中有新数据,从 dirty 中查找,开始加锁了
if !ok && read.amended {
m.mu.Lock() // 加锁
// 又在 readonly 中检查一遍,因为在加锁的时候 dirty 的数据可能已经迁移到了read中
read, _ = m.read.Load().(readOnly)
e, ok = read.m[key]
// read 还没有找到,并且dirty中有数据
if !ok && read.amended {
e, ok = m.dirty[key] //从 dirty 中查找数据
// 不管m.dirty中存不存在,都在missLocked() 中将 misses + 1。misses = len(m.dirty)时就会把 dirty 中的数据迁到 read 中
m.missLocked()
}
m.mu.Unlock()
}
if !ok {
return nil, false
}
return e.load()
}
// 如果数据不在 read 中,经过几次 miss 后,dirty 中的数据便会迁移到 read 中,这时又可以从 read 中查找到
func (m *Map) missLocked() {
m.misses++
if m.misses < len(m.dirty) { //misses 次数小于 dirty 的长度,就不迁移数据,直接返回
return
}
m.read.Store(readOnly{m: m.dirty}) //开始迁移数据
m.dirty = nil //迁移完 dirty 就赋值为 nil
m.misses = 0 //迁移完 misses 归0
}
如果 read
中已有 key
,通过 CAS
来更新值
新增 key
,或者更新 dirty
中的数据,需要加锁并写入 dirty
。其中新增 key
时还会把 read
中的 kv 倒灌回 dirty
// src/sync/map.go
// Store sets the value for a key.
func (m *Map) Store(key, value interface{}) {
// 直接在 read 中查找值,找到了,就尝试 tryStore 更新值, tryStore 里使用 for 循环 + CAS 的方式进行更新
read, _ := m.read.Load().(readOnly)
if e, ok := read.m[key]; ok && e.tryStore(&value) {
return
}
// m.read 中不存在
m.mu.Lock()
read, _ = m.read.Load().(readOnly)
if e, ok := read.m[key]; ok { // double check,因为 lock 前 read 里可能又有了
if e.unexpungeLocked() { // 未被标记成删除
m.dirty[key] = e // 加入到 dirty 里
}
e.storeLocked(&value) // 设置值
} else if e, ok := m.dirty[key]; ok { // 存在于 dirty 中,直接更新
e.storeLocked(&value)
} else { // read 和 dirty 中都没有,即插入新数据
if !read.amended { // if dirty 为空,即向 dirty 中第一次加载数据
m.dirtyLocked() // 会将 read 里的所有数据加载到 dirty 中,并将 read 里的键值标记为 expunged
m.read.Store(readOnly{m: read.m, amended: true}) // 将 read.amended 字段标记为true,下次查找会启用dirty查找
}
m.dirty[key] = newEntry(value) // 将这个entry加入到m.dirty中
}
m.mu.Unlock()
}
func (e *entry) tryStore(i *any) bool {
for {
p := atomic.LoadPointer(&e.p)
if p == expunged {
return false
}
if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
return true
}
}
}
// 遍历 read 中的 key 并写入 dirty
// 这个效率应该很差吧
func (m *Map) dirtyLocked() {
if m.dirty != nil {
return
}
read, _ := m.read.Load().(readOnly)
m.dirty = make(map[interface{}]*entry, len(read.m))
for k, e := range read.m {
if !e.tryExpungeLocked() {
m.dirty[k] = e
}
}
}
和新增差不多,只不过不设置值了,而是调用底层 map
的 delete
方法
仅建议用于如下两种场景:
- 写一次,读多次。比如程序初始化时加载配置。
- 多协程环境下,不同协程读写不同的key。
这两种场景下,相比于使用原生map + 全局锁的情况,sync.Map
能显著优化加锁带来的性能开销。
如果是读写穿插的情况,会频发触发 dirtyLocked
函数,里面会遍历 map
的 key
并进行迁移,效率非常差。
既然读写 read
可以 CAS
,那只要 read
不要 dirty
可不可以?
- 那同原生
map
就没有区别了。sync.Map
中,read
仅用于读和更新已有key
(这俩场景才能用CAS
),无法用于新增key
和触发扩容,后者的场景就需要用到dirty
了。