sync.Map 的应用场景

  • 读多写少,如缓存中。
  • 读写基本不相交,即读和写的 key 集相交部分不多。

存储结构解析

read 和 dirty 的类型

首先 Map 包含了两个存储结构 read 和 dirty,它们的最终类型都是 map[interface{}]*entry。

  1. type Map struct {
  2. mu Mutex
  3. read atomic.Value // readOnly
  4. dirty map[interface{}]*entry
  5. misses int
  6. }

明显看出 dirty 是 map[interface{}]*entry 类型,那为什么说 read 也是呢?
atomic.Value 是什么类型?就是封装了一个 interface{} 类型变量的结构体。

  1. type Value struct {
  2. v interface{}
  3. }

由此可知 read 可以接收任何类型,后面的 readOnly 注释表明,在这里 read 接收的类型是 readOnly。
那 readOnly 又是什么呢?

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

readOnly 封装了一个 map[interface{}]entry 类型的变量 m 和一个 bool 类型的 amended,因此说,read 的最终类型也是 map[interface{}]entry。
那 entry 又是什么呢?一个结构体,里面只有一个 *interface{} 类型的变量 p 即空接口指针。

  1. type entry struct {
  2. p unsafe.Pointer // *interface{}
  3. }

由此可知 entry 就是 interface{}。
因此 read 和 dirty 最终存储的 key-value 的类型是 (interface{}, interface{})。
**注意:read map 和 dirty map 的
entry 指向同一个内存空间,因此要删同时删,要更新则同时更新。**
image.png

其他字段的作用

  • 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 种情况:

  1. key 既不在 read 中也不在 dirty 中:dirty 意思性地删一下,无伤大雅,顺便 misses++。
  2. key 只在 read 中:若 p 为正常状态,则把 read 中 key 对应的 value 置为 nil,并不真正删除。若 p 为 nil,可以理解为删除无效。
  3. key 只在 dirty 中:真的把它给删掉了,并且 misses++。
  4. key 既在 read 中也在 dirty 中:把 read 中 key 对应的 value 置为 nil,并不真正删除,注意由于 read 和 map 共享 *entry 的内存空间,所以 dirty 中相同的 key 对应的 value 也为 nil。

image.png
image.png
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 中读不用加锁,性能好。
    1. func (m *Map) missLocked() {
    2. m.misses++
    3. if m.misses < len(m.dirty) {
    4. return
    5. }
    6. m.read.Store(readOnly{m: m.dirty}) // 通过原子操作添加到 read 中
    7. m.dirty = nil
    8. m.misses = 0
    9. }

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 {

    1. return

    }

    // 提升部分 read, _ := m.read.Load().(readOnly) m.dirty = make(map[interface{}]*entry, len(read.m)) for k, e := range read.m {

    1. if !e.tryExpungeLocked() {
    2. m.dirty[k] = e
    3. }

    } }

// 当 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 }

  1. <a name="6G9Yh"></a>
  2. # Load 读
  3. ```go
  4. func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
  5. read, _ := m.read.Load().(readOnly) // 类型断言获取 read map
  6. e, ok := read.m[key] // 不加锁直接读
  7. // 没读到,且 dirty map 中有 read map 没有的键值对(简称 dirty map 更新一些)
  8. // 因此要尝试到 dirty map 中读
  9. if !ok && read.amended {
  10. m.mu.Lock() // 在 dirty map 中读要上锁
  11. // 再次读 read map,因为有可能 dirty map 提升到了 read map
  12. read, _ = m.read.Load().(readOnly)
  13. e, ok = read.m[key]
  14. if !ok && read.amended { // 还是没读到,且 dirty map 更新一些
  15. e, ok = m.dirty[key] // 从 dirty map 中读
  16. m.missLocked() // 无论是否读到 misses 都加 1
  17. }
  18. m.mu.Unlock() // 解锁
  19. }
  20. // 从 dirty map 中也没读到
  21. // 说明 key 本身不存在
  22. if !ok {
  23. return nil, false
  24. }
  25. return e.load() // 无论从哪里读到的,加载它的 value 并返回
  26. }
  27. // 统计不命中次数
  28. // 并且在一定条件下发生 dirty map 提升到 read map
  29. func (m *Map) missLocked() {
  30. m.misses++ // 不命中次数加 1
  31. if m.misses < len(m.dirty) { // 如果不命中次数 >= dirty map 的长度
  32. return
  33. }
  34. m.read.Store(readOnly{m: m.dirty}) // dirty map 就会提升到 read map 中
  35. m.dirty = nil // 且 dirty map 重置为 nil
  36. m.misses = 0 // 不命中次数归 0
  37. }
  38. // 从 entry 中读取 value
  39. func (e *entry) load() (value interface{}, ok bool) {
  40. p := atomic.LoadPointer(&e.p)
  41. // 如果 value 为 nil 或者被标记为 expunged (被删除)
  42. // 说明该 key 无效
  43. if p == nil || p == expunged {
  44. return nil, false
  45. }
  46. return *(*interface{})(p), true // key 有效
  47. }

总的来说,读的流程如图所示:
image.png
读操作分 4 种情况:

  1. key 既不在 read 中也不在 dirty 中:misses++,返回 nil 和 false。
  2. key 只在 read 中:直接从 read 中不加锁地读并返回。
  3. key 只在 dirty 中:加锁后到 dirty 中读并切 misses++(加锁过程中有可能发生 dirty 到 read 的提升,因此要二次读 read)。
  4. key 既在 read 中也在 dirty 中:直接从 read 中不加锁地读并返回。

Store 存

  1. func (m *Map) Store(key, value interface{}) {
  2. read, _ := m.read.Load().(readOnly) // 类型断言,获取 read map
  3. // 如果 key 已存在 read 中,只需要更新 value
  4. // 分为两种情况:value 真实存在;value 被标记为 expunged(已删除)
  5. // 只有第一种情况能 tryStore 成功
  6. if e, ok := read.m[key]; ok && e.tryStore(&value) {
  7. return
  8. }
  9. // 到此要么 key 不存在 read 中,要么 key 存在但 value 被标记为 expunged
  10. // 需要将 value 保存到 dirty map 中,因此要加锁
  11. m.mu.Lock() // 加锁
  12. read, _ = m.read.Load().(readOnly) // 二次检查,避免刚刚 dirty map 提升为 read map
  13. if e, ok := read.m[key]; ok { // 发生了提升,且原本 dirty map 中有 key,导致 read map 现在也有
  14. if e.unexpungeLocked() { // 如果是被标记为删除 expunged 的
  15. // 说明 dirty map 不为 nil
  16. // dirty map 先前可能有这个 key 也可能没有
  17. // 但是 read map 中有,因此为了确保同步
  18. // 让 dirty map 中也有
  19. m.dirty[key] = e // m.dirty[key] = nil
  20. }
  21. e.storeLocked(&value) // 这里才是真正的保存值
  22. } else if e, ok := m.dirty[key]; ok { // key 在 dirty map 中而不在 read map
  23. e.storeLocked(&value) // 只需要更新 value 就好
  24. } else { // 两个 map 都不含有 key
  25. if !read.amended { // 如果两个 map 一样新
  26. m.dirtyLocked()
  27. m.read.Store(readOnly{m: read.m, amended: true})
  28. }
  29. m.dirty[key] = newEntry(value)
  30. }
  31. m.mu.Unlock()
  32. }
  33. // 当且仅当 entry 没有被标记时
  34. // tryStore 才会保存 value
  35. func (e *entry) tryStore(i *interface{}) bool {
  36. for {
  37. p := atomic.LoadPointer(&e.p)
  38. // 被标记为 expunged,表示已删除,不保存
  39. if p == expunged {
  40. return false
  41. }
  42. // 成功保存了 value
  43. if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
  44. return true
  45. }
  46. }
  47. }
  48. // 当条目被标记为 expunged 时把该条目置为 nil 并返回 true
  49. // 否则返回 false
  50. func (e *entry) unexpungeLocked() (wasExpunged bool) {
  51. return atomic.CompareAndSwapPointer(&e.p, expunged, nil)
  52. }
  53. //
  54. func (m *Map) dirtyLocked() {
  55. // dirty != nil 说明dirty在上次read同步dirty数据后已经有了修改了,这时候read的数据不一定准确,不能同步
  56. if m.dirty != nil {
  57. return
  58. }
  59. read, _ := m.read.Load().(readOnly)
  60. m.dirty = make(map[interface{}]*entry, len(read.m))
  61. for k, e := range read.m {
  62. if !e.tryExpungeLocked() {
  63. m.dirty[k] = e
  64. }
  65. }
  66. }
  67. // e.p == nil 或 e.p == expunged 时返回 true
  68. func (e *entry) tryExpungeLocked() (isExpunged bool) {
  69. p := atomic.LoadPointer(&e.p)
  70. for p == nil {
  71. if atomic.CompareAndSwapPointer(&e.p, nil, expunged) {
  72. return true
  73. }
  74. p = atomic.LoadPointer(&e.p)
  75. }
  76. return p == expunged
  77. }

存操作都会先通过读操作判断 key 是否已存在。
存操作分 4 种情况:

  1. key 既不在 read 中也不在 dirty 中:若 amended 为 false,先把 read 提升到 dirty,再直接存到 dirty 中。
  2. key 只在 read 中:若 p 不为 expunged 则只更新 read 中对应的 p,否则把 p 置为 nil,然后加锁存在 dirty 中(加锁过程中有可能发生 dirty 到 read 的提升,因此要二次读 read),由于共享 p 因此 read 也保存了。
  3. key 只在 dirty 中:加锁存到 dirty 中。
  4. key 既在 read 中也在 dirty 中:这种情况下 p 只可能是 nil 和正常状态,直接更新 read 中的 p。

Delete 删

  1. // Delete deletes the value for a key.
  2. func (m *Map) Delete(key interface{}) {
  3. m.LoadAndDelete(key)
  4. }
  5. func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool) {
  6. read, _ := m.read.Load().(readOnly)
  7. e, ok := read.m[key]
  8. if !ok && read.amended {
  9. m.mu.Lock()
  10. read, _ = m.read.Load().(readOnly)
  11. e, ok = read.m[key]
  12. if !ok && read.amended {
  13. e, ok = m.dirty[key]
  14. delete(m.dirty, key)
  15. // Regardless of whether the entry was present, record a miss: this key
  16. // will take the slow path until the dirty map is promoted to the read
  17. // map.
  18. m.missLocked()
  19. }
  20. m.mu.Unlock()
  21. }
  22. if ok {
  23. return e.delete()
  24. }
  25. return nil, false
  26. }
  27. func (e *entry) delete() (value interface{}, ok bool) {
  28. for {
  29. p := atomic.LoadPointer(&e.p)
  30. if p == nil || p == expunged {
  31. return nil, false
  32. }
  33. if atomic.CompareAndSwapPointer(&e.p, p, nil) {
  34. return *(*interface{})(p), true
  35. }
  36. }
  37. }

删操作之前都要执行读操作
删除的 4 种情况见上文“read 和 dirty 的存储内容”。

Range 操作

  1. func (m *Map) Range(f func(key, value interface{}) bool) {
  2. read, _ := m.read.Load().(readOnly)
  3. if read.amended {
  4. m.mu.Lock()
  5. read, _ = m.read.Load().(readOnly)
  6. if read.amended {
  7. read = readOnly{m: m.dirty}
  8. m.read.Store(read)
  9. m.dirty = nil
  10. m.misses = 0
  11. }
  12. m.mu.Unlock()
  13. }
  14. for k, e := range read.m {
  15. v, ok := e.load()
  16. if !ok {
  17. continue
  18. }
  19. if !f(k, v) {
  20. break
  21. }
  22. }
  23. }

需要自己传入一个 f func(key, value interface{}) bool 函数,并且要 return true 才能遍历。
只遍历 read 结构,不过在这之前如果 dirty 更新,就先把 dirty 提升到 read。
对于 p 为 nil 和 expunged 的键值对不会输出,可以理解为只输出正常内容。

其他

  • 获取 Map 的长度。没有提供包含内容数量的函数,可以利用 Range 遍历时,自己传入的函数来统计。
  • LoadOrStore 方法:尝试读取
  • LoadAndDelete 方法:

总结

适用场景

再次强调 sync.Map 的适用场景:

  • 读多写少。
  • 读写基本不相交,即读写所用的两个 key 集合交集不多。

优点

  1. 利用空间换时间。引入 read 作为缓存结构,通过原子操作而不是加锁地读或写,提供较好的读性能。
  2. 动态调整缓存。当 misses 过多时,发生 dirty 提升到 read,保证 read 有较高的命中率,维持稳定的性能。
  3. 优先从 read 中进行读写。对 read 进行读写只需要通过原子操作而不用加锁,性能较好。

缺点

  1. 不适用于大量存储和删除的场景,因为这两种操作都会有大概率上锁操作 dirty。