sync.Map 的应用场景
- 读多写少,如缓存中。
- 读写基本不相交,即读和写的 key 集相交部分不多。
存储结构解析
read 和 dirty 的类型
首先 Map 包含了两个存储结构 read 和 dirty,它们的最终类型都是 map[interface{}]*entry。
type Map struct {
mu Mutex
read atomic.Value // readOnly
dirty map[interface{}]*entry
misses int
}
明显看出 dirty 是 map[interface{}]*entry 类型,那为什么说 read 也是呢?
atomic.Value 是什么类型?就是封装了一个 interface{} 类型变量的结构体。
type Value struct {
v interface{}
}
由此可知 read 可以接收任何类型,后面的 readOnly 注释表明,在这里 read 接收的类型是 readOnly。
那 readOnly 又是什么呢?
type readOnly struct {
m map[interface{}]*entry
amended bool // true if the dirty map contains some key not in m.
}
readOnly 封装了一个 map[interface{}]entry 类型的变量 m 和一个 bool 类型的 amended,因此说,read 的最终类型也是 map[interface{}]entry。
那 entry 又是什么呢?一个结构体,里面只有一个 *interface{} 类型的变量 p 即空接口指针。
type entry struct {
p unsafe.Pointer // *interface{}
}
由此可知 entry 就是 interface{}。
因此 read 和 dirty 最终存储的 key-value 的类型是 (interface{}, interface{})。
**注意:read map 和 dirty map 的 entry 指向同一个内存空间,因此要删同时删,要更新则同时更新。**
其他字段的作用
- amended 是一个标识符,当 dirty map 中有 read map 没有的键值对(简称 dirty map 更新一些)时为 true。
- mu 是一个互斥锁,从 dirty map 中读时需要上锁。
为什么要引入 atomic.Value 存储 map
因为需要利用 atomic 里面的各种原子操作,比如原子读,原子写,原子交换等,来实现 read 结构不加锁的并发读写等,详见Go 并发中“原子操作”部分。
为什么要用两个 map
因为利用两个 map 构成缓存!read map 相当于 dirty map 的缓存。
- 缓存 read map 读写,不用上锁,性能好。
- 缓存不命中,要从 dirty map 中读,需要上锁。
- 只要缓存不命中,misses 就加 1,当 misses 次数大于或等于 dirty map 的长度时,发生 dirty map 提升到 read map(就是把 dirty map 中的内容全部放到 read map 中并且 dirty map 清空,misses 也归零)。
read 和 dirty 存储的内容
由于 read 相当于缓存,在执行删除操作时,有些特殊。
删除有 4 种情况:
- key 既不在 read 中也不在 dirty 中:dirty 意思性地删一下,无伤大雅,顺便 misses++。
- key 只在 read 中:若 p 为正常状态,则把 read 中 key 对应的 value 置为 nil,并不真正删除。若 p 为 nil,可以理解为删除无效。
- key 只在 dirty 中:真的把它给删掉了,并且 misses++。
- key 既在 read 中也在 dirty 中:把 read 中 key 对应的 value 置为 nil,并不真正删除,注意由于 read 和 map 共享 *entry 的内存空间,所以 dirty 中相同的 key 对应的 value 也为 nil。
read 中的 p 具有以上 3 种状态。
dirty 中的 p 只有 2 种,没有 expunged。
提升
dirty 提升到 read
- 过程:直接把 dirty 中的内容添加到 read 中,然后令 dirty 为 nil,misses 归零。
- 触发时机:
misses >= len(dirty)
,在 missLock 方法中。 - 导致 misses 增加的原因:
- Load 读操作时,key 不在 read 中。
- Delete 删操作时,key 不在 read 中。
- 提升的原因:提高命中率,提供较好的性能。因为过多的 misses 说明当前要读取的数据都在 dirty 中,从 dirty 中读是需要加锁的,性能较差,而从 read 中读不用加锁,性能好。
func (m *Map) missLocked() {
m.misses++
if m.misses < len(m.dirty) {
return
}
m.read.Store(readOnly{m: m.dirty}) // 通过原子操作添加到 read 中
m.dirty = nil
m.misses = 0
}
read 提升到 dirty
- 过程:把 read 中 p 为正常状态的内容复制到 dirty 中,并且把 read 中 p 为 nil 改为 expunged。
- 触发时机:Store 存操作时,key 既不在 read 中也不在 dirty 中,且 dirty 为 nil,此时 amended 为 false,说明要么 read 的内容和 dirty 一样,要么 read 的内容更新一些(比如 Store 操作更新了 value)。
提升的原因:dirty 为 nil,初始化 dirty ```go func (m *Map) dirtyLocked() { // dirty 不为 nil,不发生提升 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
}
} }
// 当 e.p == nil 或 e.p == expunged 时,返回 true func (e *entry) tryExpungeLocked() (isExpunged bool) { p := atomic.LoadPointer(&e.p) for p == nil { if atomic.CompareAndSwapPointer(&e.p, nil, expunged) { // 原子 CAS 操作 return true } p = atomic.LoadPointer(&e.p) } return p == expunged }
<a name="6G9Yh"></a>
# Load 读
```go
func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
read, _ := m.read.Load().(readOnly) // 类型断言获取 read map
e, ok := read.m[key] // 不加锁直接读
// 没读到,且 dirty map 中有 read map 没有的键值对(简称 dirty map 更新一些)
// 因此要尝试到 dirty map 中读
if !ok && read.amended {
m.mu.Lock() // 在 dirty map 中读要上锁
// 再次读 read map,因为有可能 dirty map 提升到了 read map
read, _ = m.read.Load().(readOnly)
e, ok = read.m[key]
if !ok && read.amended { // 还是没读到,且 dirty map 更新一些
e, ok = m.dirty[key] // 从 dirty map 中读
m.missLocked() // 无论是否读到 misses 都加 1
}
m.mu.Unlock() // 解锁
}
// 从 dirty map 中也没读到
// 说明 key 本身不存在
if !ok {
return nil, false
}
return e.load() // 无论从哪里读到的,加载它的 value 并返回
}
// 统计不命中次数
// 并且在一定条件下发生 dirty map 提升到 read map
func (m *Map) missLocked() {
m.misses++ // 不命中次数加 1
if m.misses < len(m.dirty) { // 如果不命中次数 >= dirty map 的长度
return
}
m.read.Store(readOnly{m: m.dirty}) // dirty map 就会提升到 read map 中
m.dirty = nil // 且 dirty map 重置为 nil
m.misses = 0 // 不命中次数归 0
}
// 从 entry 中读取 value
func (e *entry) load() (value interface{}, ok bool) {
p := atomic.LoadPointer(&e.p)
// 如果 value 为 nil 或者被标记为 expunged (被删除)
// 说明该 key 无效
if p == nil || p == expunged {
return nil, false
}
return *(*interface{})(p), true // key 有效
}
总的来说,读的流程如图所示:
读操作分 4 种情况:
- key 既不在 read 中也不在 dirty 中:misses++,返回 nil 和 false。
- key 只在 read 中:直接从 read 中不加锁地读并返回。
- key 只在 dirty 中:加锁后到 dirty 中读并切 misses++(加锁过程中有可能发生 dirty 到 read 的提升,因此要二次读 read)。
- key 既在 read 中也在 dirty 中:直接从 read 中不加锁地读并返回。
Store 存
func (m *Map) Store(key, value interface{}) {
read, _ := m.read.Load().(readOnly) // 类型断言,获取 read map
// 如果 key 已存在 read 中,只需要更新 value
// 分为两种情况:value 真实存在;value 被标记为 expunged(已删除)
// 只有第一种情况能 tryStore 成功
if e, ok := read.m[key]; ok && e.tryStore(&value) {
return
}
// 到此要么 key 不存在 read 中,要么 key 存在但 value 被标记为 expunged
// 需要将 value 保存到 dirty map 中,因此要加锁
m.mu.Lock() // 加锁
read, _ = m.read.Load().(readOnly) // 二次检查,避免刚刚 dirty map 提升为 read map
if e, ok := read.m[key]; ok { // 发生了提升,且原本 dirty map 中有 key,导致 read map 现在也有
if e.unexpungeLocked() { // 如果是被标记为删除 expunged 的
// 说明 dirty map 不为 nil
// dirty map 先前可能有这个 key 也可能没有
// 但是 read map 中有,因此为了确保同步
// 让 dirty map 中也有
m.dirty[key] = e // m.dirty[key] = nil
}
e.storeLocked(&value) // 这里才是真正的保存值
} else if e, ok := m.dirty[key]; ok { // key 在 dirty map 中而不在 read map
e.storeLocked(&value) // 只需要更新 value 就好
} else { // 两个 map 都不含有 key
if !read.amended { // 如果两个 map 一样新
m.dirtyLocked()
m.read.Store(readOnly{m: read.m, amended: true})
}
m.dirty[key] = newEntry(value)
}
m.mu.Unlock()
}
// 当且仅当 entry 没有被标记时
// tryStore 才会保存 value
func (e *entry) tryStore(i *interface{}) bool {
for {
p := atomic.LoadPointer(&e.p)
// 被标记为 expunged,表示已删除,不保存
if p == expunged {
return false
}
// 成功保存了 value
if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
return true
}
}
}
// 当条目被标记为 expunged 时把该条目置为 nil 并返回 true
// 否则返回 false
func (e *entry) unexpungeLocked() (wasExpunged bool) {
return atomic.CompareAndSwapPointer(&e.p, expunged, nil)
}
//
func (m *Map) dirtyLocked() {
// dirty != nil 说明dirty在上次read同步dirty数据后已经有了修改了,这时候read的数据不一定准确,不能同步
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
}
}
}
// e.p == nil 或 e.p == expunged 时返回 true
func (e *entry) tryExpungeLocked() (isExpunged bool) {
p := atomic.LoadPointer(&e.p)
for p == nil {
if atomic.CompareAndSwapPointer(&e.p, nil, expunged) {
return true
}
p = atomic.LoadPointer(&e.p)
}
return p == expunged
}
存操作都会先通过读操作判断 key 是否已存在。
存操作分 4 种情况:
- key 既不在 read 中也不在 dirty 中:若 amended 为 false,先把 read 提升到 dirty,再直接存到 dirty 中。
- key 只在 read 中:若 p 不为 expunged 则只更新 read 中对应的 p,否则把 p 置为 nil,然后加锁存在 dirty 中(加锁过程中有可能发生 dirty 到 read 的提升,因此要二次读 read),由于共享 p 因此 read 也保存了。
- key 只在 dirty 中:加锁存到 dirty 中。
- key 既在 read 中也在 dirty 中:这种情况下 p 只可能是 nil 和正常状态,直接更新 read 中的 p。
Delete 删
// Delete deletes the value for a key.
func (m *Map) Delete(key interface{}) {
m.LoadAndDelete(key)
}
func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool) {
read, _ := m.read.Load().(readOnly)
e, ok := read.m[key]
if !ok && read.amended {
m.mu.Lock()
read, _ = m.read.Load().(readOnly)
e, ok = read.m[key]
if !ok && read.amended {
e, ok = m.dirty[key]
delete(m.dirty, key)
// Regardless of whether the entry was present, record a miss: this key
// will take the slow path until the dirty map is promoted to the read
// map.
m.missLocked()
}
m.mu.Unlock()
}
if ok {
return e.delete()
}
return nil, false
}
func (e *entry) delete() (value interface{}, ok bool) {
for {
p := atomic.LoadPointer(&e.p)
if p == nil || p == expunged {
return nil, false
}
if atomic.CompareAndSwapPointer(&e.p, p, nil) {
return *(*interface{})(p), true
}
}
}
删操作之前都要执行读操作
删除的 4 种情况见上文“read 和 dirty 的存储内容”。
Range 操作
func (m *Map) Range(f func(key, value interface{}) bool) {
read, _ := m.read.Load().(readOnly)
if read.amended {
m.mu.Lock()
read, _ = m.read.Load().(readOnly)
if read.amended {
read = readOnly{m: m.dirty}
m.read.Store(read)
m.dirty = nil
m.misses = 0
}
m.mu.Unlock()
}
for k, e := range read.m {
v, ok := e.load()
if !ok {
continue
}
if !f(k, v) {
break
}
}
}
需要自己传入一个 f func(key, value interface{}) bool
函数,并且要 return true 才能遍历。
只遍历 read 结构,不过在这之前如果 dirty 更新,就先把 dirty 提升到 read。
对于 p 为 nil 和 expunged 的键值对不会输出,可以理解为只输出正常内容。
其他
- 获取 Map 的长度。没有提供包含内容数量的函数,可以利用 Range 遍历时,自己传入的函数来统计。
- LoadOrStore 方法:尝试读取
- LoadAndDelete 方法:
总结
适用场景
再次强调 sync.Map 的适用场景:
- 读多写少。
- 读写基本不相交,即读写所用的两个 key 集合交集不多。
优点
- 利用空间换时间。引入 read 作为缓存结构,通过原子操作而不是加锁地读或写,提供较好的读性能。
- 动态调整缓存。当 misses 过多时,发生 dirty 提升到 read,保证 read 有较高的命中率,维持稳定的性能。
- 优先从 read 中进行读写。对 read 进行读写只需要通过原子操作而不用加锁,性能较好。
缺点
- 不适用于大量存储和删除的场景,因为这两种操作都会有大概率上锁操作 dirty。