go src - sync.Map

打印 上一主题 下一主题

主题 889|帖子 889|积分 2667

前言

在并发编程中,我们经常会遇到多个goroutine同时操作一个map的情况。如果在这种情况下直接使用普通的map,那么就可能会引发竞态条件,造成数据不一致或者更严重的问题。
sync.Map是Go语言中内置的一种并发安全的map,但是他的实现和用法与普通的map完全不同,这篇文章将详细介绍这些区别。
一、使用方法

创建sync.Map非常简单,只需要声明即可:
  1. var m sync.Map
复制代码
使用Store方法存储键值对:
  1. m.Store("hello", "world")
复制代码
使用Load方法获取值:
  1. value, ok := m.Load("hello")
  2. if ok {
  3.     fmt.Println(value) // 输出:world
  4. }
复制代码
使用Delete方法删除键值对:
  1. m.Delete("hello")
复制代码
二、原理

sync.Map的核心实现依赖于两个主要的数据结构:一个只读的read字段,以及一个可写的dirty字段。
读操作:

当我们进行读取操作(Load)时,首先会尝试从read字段读取数据,这个过程是完全无锁的。
如果read中没有找到,那么会尝试加锁后从dirty中读取。这个设计保证了在大部分读多写少的场景下,读操作是无锁的,大大提升了性能。
写操作:

写入操作(key不存在)时,会直接在dirty中进行写入,并将read的amended标记为true,表示read字段有待更新的数据。
然后再有新的读取操作到来时,如果amended为true并且miss数量超过dirty长度,则会从dirty中拷贝数据到read,并清除amended标记。
 总结:

在这个设计中,读操作在大部分情况下是无锁的,而写操作(key不存在时)则需要获取dirty的锁,从而实现了对于读多写少场景的优化。
三、优点

sync.Map在以下两种情况下表现得特别好:

  • 键值对的数量相对稳定,读操作明显多于写操作的场景
  • 多个goroutine并发读取不重叠的键集的场景
这是因为sync.Map的设计将读取操作优化至极致,同时尽量减少在写入新键值对时的锁竞争。
四、缺点

然而,sync.Map并不是银弹,也有一些局限:

  • sync.Map没有像普通map那样的直观语法,必须使用特定的方法来操作键值对
  • 对于键值对数量快速增长、写操作频繁的场景,sync.Map的性能可能不如使用普通map加锁的方式
  • 读操作无锁情况下,可能会出现时间竞态问题
五、实现

 sync.Map
  1. type Map struct {
  2.         mu Mutex
  3.         // read contains the portion of the map's contents that are safe for
  4.         // concurrent access (with or without mu held).
  5.         read atomic.Pointer[readOnly]
  6.         // dirty contains the portion of the map's contents that require mu to be
  7.         // held. To ensure that the dirty map can be promoted to the read map quickly,
  8.         // it also includes all of the non-expunged entries in the read map.
  9.         dirty map[any]*entry
  10.         // misses counts the number of loads since the read map was last updated that
  11.         // needed to lock mu to determine whether the key was present.
  12.         misses int
  13. }
复制代码
readonly
  1. // readOnly is an immutable struct stored atomically in the Map.read field.
  2. type readOnly struct {
  3.         m       map[any]*entry
  4.         amended bool // true if the dirty map contains some key not in m.
  5. }
复制代码
entry
  1. // An entry is a slot in the map corresponding to a particular key.
  2. type entry struct {
  3.         // p points to the interface{} value stored for the entry.
  4.         p atomic.Pointer[any]
  5. }
复制代码

expunged
  1. // expunged is an arbitrary pointer that marks entries which have been deleted
  2. // from the dirty map.
  3. var expunged = new(any)
复制代码
状态机:

 总结:

  • 当key从read中删除时,会先被标记为nil,不会立马删除key
  • 当重新初始化dirty时(将read.m克隆到dirty),如果key的值为nil,会设置为expunged,并不在dirty中创建这个key。
  • 如果key为expunged,LoadOrStore/Swap/CompareAndDelete/CompareAndSwap都会不执行写操作并返回false。
Load
  1. // Load returns the value stored in the map for a key, or nil if no
  2. // value is present.
  3. // The ok result indicates whether value was found in the map.
  4. func (m *Map) Load(key any) (value any, ok bool) {
  5.         read := m.loadReadOnly()
  6.         e, ok := read.m[key]
  7.         if !ok && read.amended {
  8.                 m.mu.Lock()
  9.                 // Avoid reporting a spurious miss if m.dirty got promoted while we were
  10.                 // blocked on m.mu. (If further loads of the same key will not miss, it's
  11.                 // not worth copying the dirty map for this key.)
  12.                 read = m.loadReadOnly()
  13.                 e, ok = read.m[key]
  14.                 if !ok && read.amended {
  15.                         e, ok = m.dirty[key]
  16.                         // Regardless of whether the entry was present, record a miss: this key
  17.                         // will take the slow path until the dirty map is promoted to the read
  18.                         // map.
  19.                         m.missLocked()
  20.                 }
  21.                 m.mu.Unlock()
  22.         }
  23.         if !ok {
  24.                 return nil, false
  25.         }
  26.         return e.load()
  27. }
  28. func (m *Map) loadReadOnly() readOnly {
  29.         if p := m.read.Load(); p != nil {
  30.                 return *p
  31.         }
  32.         return readOnly{}
  33. }
复制代码

 总结:

  • 如果查询的key在read中找到了,返回entry.load()
  • 如果查询的key在read中未找到,并且read和dirty一致,返回nil, false
  • key未找到,并且read与dirty不一致

    • 加锁
    • 重新查询read,类似上面1、2流程,如果未找到并且read和dirty不一致则继续
    • 在dirty中查询
    • misses加一
    • 如果misses数大于dirty长度,将dirty同步到read,重置dirty和misses
    • 释放锁
    • 返回结果

LoadOrStore
  1. // LoadOrStore returns the existing value for the key if present.
  2. // Otherwise, it stores and returns the given value.
  3. // The loaded result is true if the value was loaded, false if stored.
  4. func (m *Map) LoadOrStore(key, value any) (actual any, loaded bool) {
  5.         // Avoid locking if it's a clean hit.
  6.         read := m.loadReadOnly()
  7.         if e, ok := read.m[key]; ok {
  8.                 actual, loaded, ok := e.tryLoadOrStore(value)
  9.                 if ok {
  10.                         return actual, loaded
  11.                 }
  12.         }
  13.         m.mu.Lock()
  14.         read = m.loadReadOnly()
  15.         if e, ok := read.m[key]; ok {
  16.                 if e.unexpungeLocked() {
  17.                         m.dirty[key] = e
  18.                 }
  19.                 actual, loaded, _ = e.tryLoadOrStore(value)
  20.         } else if e, ok := m.dirty[key]; ok {
  21.                 actual, loaded, _ = e.tryLoadOrStore(value)
  22.                 m.missLocked()
  23.         } else {
  24.                 if !read.amended {
  25.                         // We're adding the first new key to the dirty map.
  26.                         // Make sure it is allocated and mark the read-only map as incomplete.
  27.                         m.dirtyLocked()
  28.                         m.read.Store(&readOnly{m: read.m, amended: true})
  29.                 }
  30.                 m.dirty[key] = newEntry(value)
  31.                 actual, loaded = value, false
  32.         }
  33.         m.mu.Unlock()
  34.         return actual, loaded
  35. }
  36. // tryLoadOrStore atomically loads or stores a value if the entry is not
  37. // expunged.
  38. //
  39. // If the entry is expunged, tryLoadOrStore leaves the entry unchanged and
  40. // returns with ok==false.
  41. func (e *entry) tryLoadOrStore(i any) (actual any, loaded, ok bool) {
  42.         p := e.p.Load()
  43.         if p == expunged {
  44.                 return nil, false, false
  45.         }
  46.         if p != nil {
  47.                 return *p, true, true
  48.         }
  49.         // Copy the interface after the first load to make this method more amenable
  50.         // to escape analysis: if we hit the "load" path or the entry is expunged, we
  51.         // shouldn't bother heap-allocating.
  52.         ic := i
  53.         for {
  54.                 if e.p.CompareAndSwap(nil, &ic) {
  55.                         return i, false, true
  56.                 }
  57.                 p = e.p.Load()
  58.                 if p == expunged {
  59.                         return nil, false, false
  60.                 }
  61.                 if p != nil {
  62.                         return *p, true, true
  63.                 }
  64.         }
  65. }
复制代码

 总结:

  • 如果key在read中找到了,并且不为expunged

    • 如果为nil,则CAS新的值,并返回value, false
    • 如果不为nil,则返回*p, true

  • 如果key在read中不存在,或者为expunged

    • 加锁
    • 再次在read中查找,如果找到了

      • 如果为expunged,结果为nil, false
      • 如果为nil,则CAS新的值,结果为value, false
      • 如果不为nil,结果为*p, true

    • 如果在dirty中找到了,重复2的逻辑判断
    • 在read和dirty中都没有,则创建一个新的entry
    • 释放锁
    • 返回结果

Delete/LoadAndDelete
  1. // Delete deletes the value for a key.
  2. func (m *Map) Delete(key any) {
  3.         m.LoadAndDelete(key)
  4. }
复制代码
  1. // LoadAndDelete deletes the value for a key, returning the previous value if any.
  2. // The loaded result reports whether the key was present.
  3. func (m *Map) LoadAndDelete(key any) (value any, loaded bool) {
  4.         read := m.loadReadOnly()
  5.         e, ok := read.m[key]
  6.         if !ok && read.amended {
  7.                 m.mu.Lock()
  8.                 read = m.loadReadOnly()
  9.                 e, ok = read.m[key]
  10.                 if !ok && read.amended {
  11.                         e, ok = m.dirty[key]
  12.                         delete(m.dirty, key)
  13.                         // Regardless of whether the entry was present, record a miss: this key
  14.                         // will take the slow path until the dirty map is promoted to the read
  15.                         // map.
  16.                         m.missLocked()
  17.                 }
  18.                 m.mu.Unlock()
  19.         }
  20.         if ok {
  21.                 return e.delete()
  22.         }
  23.         return nil, false
  24. }
复制代码

 总结:

  • 如果要删除的key在read中存在,将它置为nil
  • 如果要删除的key在read中未找到,并且read和dirty一致,说明key不存在,返回nil, false
  • key未找到,并且read和dirty不一致

    • 加锁
    • 重新查询read,类似上面1、2流程,如果key未找到,并且read和dirty不一致继续
    • 在dirty中查询并删除
    • misses加一
    • 如果misses数大于dirty长度,将dirty同步到read,重置dirty和misses
    • 释放锁
    • 如果key在dirty中也不存在,返回nil, false;反之,将它置为nil

 Store/Swap
  1. // Store sets the value for a key.
  2. func (m *Map) Store(key, value any) {
  3.         _, _ = m.Swap(key, value)
  4. }
  5. // Swap swaps the value for a key and returns the previous value if any.
  6. // The loaded result reports whether the key was present.
  7. func (m *Map) Swap(key, value any) (previous any, loaded bool) {
  8.         read := m.loadReadOnly()
  9.         if e, ok := read.m[key]; ok {
  10.                 if v, ok := e.trySwap(&value); ok {
  11.                         if v == nil {
  12.                                 return nil, false
  13.                         }
  14.                         return *v, true
  15.                 }
  16.         }
  17.         m.mu.Lock()
  18.         read = m.loadReadOnly()
  19.         if e, ok := read.m[key]; ok {
  20.                 if e.unexpungeLocked() {
  21.                         // The entry was previously expunged, which implies that there is a
  22.                         // non-nil dirty map and this entry is not in it.
  23.                         m.dirty[key] = e
  24.                 }
  25.                 if v := e.swapLocked(&value); v != nil {
  26.                         loaded = true
  27.                         previous = *v
  28.                 }
  29.         } else if e, ok := m.dirty[key]; ok {
  30.                 if v := e.swapLocked(&value); v != nil {
  31.                         loaded = true
  32.                         previous = *v
  33.                 }
  34.         } else {
  35.                 if !read.amended {
  36.                         // We're adding the first new key to the dirty map.
  37.                         // Make sure it is allocated and mark the read-only map as incomplete.
  38.                         m.dirtyLocked()
  39.                         m.read.Store(&readOnly{m: read.m, amended: true})
  40.                 }
  41.                 m.dirty[key] = newEntry(value)
  42.         }
  43.         m.mu.Unlock()
  44.         return previous, loaded
  45. }
  46. // trySwap swaps a value if the entry has not been expunged.
  47. //
  48. // If the entry is expunged, trySwap returns false and leaves the entry
  49. // unchanged.
  50. func (e *entry) trySwap(i *any) (*any, bool) {
  51.         for {
  52.                 p := e.p.Load()
  53.                 if p == expunged {
  54.                         return nil, false
  55.                 }
  56.                 if e.p.CompareAndSwap(p, i) {
  57.                         return p, true
  58.                 }
  59.         }
  60. }
复制代码

总结:

  • 如果key在read中找到了,并且不为expunged,则试图CAS并返回结果
  • key在read中未找到,或者为expunged

    • 加锁
    • 在read中查询

      • 如果查到了,试图unexpunge

        • 如果需要unexpunge,会将entry置(CAS)为nil,并在dirty中插入key
        • 执行entry的Swap


    • 在ditry中查询

      • 如果查到了,执行entry的Swap

    • 都没查到,则检查dirty是否存在,初始化dirty,并在dirty增加新的entry
    • 释放锁
    • 返回结果

CompareAndSwap
  1. // CompareAndSwap swaps the old and new values for key
  2. // if the value stored in the map is equal to old.
  3. // The old value must be of a comparable type.
  4. func (m *Map) CompareAndSwap(key, old, new any) bool {
  5.         read := m.loadReadOnly()
  6.         if e, ok := read.m[key]; ok {
  7.                 return e.tryCompareAndSwap(old, new)
  8.         } else if !read.amended {
  9.                 return false // No existing value for key.
  10.         }
  11.         m.mu.Lock()
  12.         defer m.mu.Unlock()
  13.         read = m.loadReadOnly()
  14.         swapped := false
  15.         if e, ok := read.m[key]; ok {
  16.                 swapped = e.tryCompareAndSwap(old, new)
  17.         } else if e, ok := m.dirty[key]; ok {
  18.                 swapped = e.tryCompareAndSwap(old, new)
  19.                 // We needed to lock mu in order to load the entry for key,
  20.                 // and the operation didn't change the set of keys in the map
  21.                 // (so it would be made more efficient by promoting the dirty
  22.                 // map to read-only).
  23.                 // Count it as a miss so that we will eventually switch to the
  24.                 // more efficient steady state.
  25.                 m.missLocked()
  26.         }
  27.         return swapped
  28. }
  29. // tryCompareAndSwap compare the entry with the given old value and swaps
  30. // it with a new value if the entry is equal to the old value, and the entry
  31. // has not been expunged.
  32. //
  33. // If the entry is expunged, tryCompareAndSwap returns false and leaves
  34. // the entry unchanged.
  35. func (e *entry) tryCompareAndSwap(old, new any) bool {
  36.         p := e.p.Load()
  37.         if p == nil || p == expunged || *p != old {
  38.                 return false
  39.         }
  40.         // Copy the interface after the first load to make this method more amenable
  41.         // to escape analysis: if the comparison fails from the start, we shouldn't
  42.         // bother heap-allocating an interface value to store.
  43.         nc := new
  44.         for {
  45.                 if e.p.CompareAndSwap(p, &nc) {
  46.                         return true
  47.                 }
  48.                 p = e.p.Load()
  49.                 if p == nil || p == expunged || *p != old {
  50.                         return false
  51.                 }
  52.         }
  53. }
复制代码

 总结:

  • 如果key在read中找到了

    • 如果为nil/expunged/不等于old,则返回false
    • 试图进行entry的CAS,成功返回true,失败继续1、2流程

  • 如果key在read中未找到,并且read与dirty没有不一致,返回false
  • 如果key在read中未找到,并且read与dirty不一致

    • 加锁
    • 如果key在read中找到

      • 如果仍然为nil/expunged/不等于old,结果为false
      • 试图进行entry的CAS,CAS成功,则结果为true;否则继续1、2流程

    • 如果key在dirty中找到

      • 如果不等于old,结果为false
      • 试图进行entry的CAS,CAS成功,则结果为true;否则继续1、2流程
      • misses加一
      • 如果misses数大于dirty长度,将dirty同步到read,重置dirty和misses

    • 释放锁
    • 返回结果

CompareAndDelete
  1. // CompareAndDelete deletes the entry for key if its value is equal to old.
  2. // The old value must be of a comparable type.
  3. //
  4. // If there is no current value for key in the map, CompareAndDelete
  5. // returns false (even if the old value is the nil interface value).
  6. func (m *Map) CompareAndDelete(key, old any) (deleted bool) {
  7.         read := m.loadReadOnly()
  8.         e, ok := read.m[key]
  9.         if !ok && read.amended {
  10.                 m.mu.Lock()
  11.                 read = m.loadReadOnly()
  12.                 e, ok = read.m[key]
  13.                 if !ok && read.amended {
  14.                         e, ok = m.dirty[key]
  15.                         // Don't delete key from m.dirty: we still need to do the “compare” part
  16.                         // of the operation. The entry will eventually be expunged when the
  17.                         // dirty map is promoted to the read map.
  18.                         //
  19.                         // Regardless of whether the entry was present, record a miss: this key
  20.                         // will take the slow path until the dirty map is promoted to the read
  21.                         // map.
  22.                         m.missLocked()
  23.                 }
  24.                 m.mu.Unlock()
  25.         }
  26.         for ok {
  27.                 p := e.p.Load()
  28.                 if p == nil || p == expunged || *p != old {
  29.                         return false
  30.                 }
  31.                 if e.p.CompareAndSwap(p, nil) {
  32.                         return true
  33.                 }
  34.         }
  35.         return false
  36. }
复制代码

总结:

  • 如果key在read中找到了

    • 如果为nil/expunged/不等于old,则返回false
    • 试图进行entry的CAS,成功返回true,失败继续1、2流程

  • 如果key在read中未找到,并且read和dirty没有不一致,返回false
  • 如果key在read中未找到,并且read和dirty不一致

    • 加锁
    • 从read中查找

      • 如果找到key

        • 为nil/expunged/不等于old,结果为false
        • 试图进行CAS,成功结果为true,失败继续尝试

      • 如果没找到,并且没有不一致,结果为false

    • 如果read和dirty有不一致

      • 从dirty中查找
      • 如果找到key

        • 不等于old,结果为false
        • 试图CAS,成功结果为true,失败继续尝试

      • 没找到key,结果为false

    • 释放锁
    • 返回结果

结论
总的来说,sync.Map是Go标准库提供的一个非常有用的工具,它可以帮助我们简化并发编程,并且在一些特定的场景下能提供良好的性能。
但在使用的时候,我们需要根据具体的应用场景和需求来选择使用sync.Map还是其他的并发原语。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

小秦哥

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表