sync.Map

警告
本文最后更新于 2023-02-16,文中内容可能已过时。

sync.Map 是Go语言中提供的一种大幅度改进版本的share.Map,它实现了在多个协程中安全读写map的功能。它在内部使用了 Mutex (互斥锁) 来实现数据的安全性,提供的功能如下:

  • 快速:它使用了诸如读写加锁权衡,把锁分散,采用读更新一小片的方式,使得整体性能比其他实现更好。
  • 安全:它的读写都是安全的,因此它可以作为在多个协程中安全进行读写的工具。

sync.Map 类型主要针对以下场景进行了性能优化:

  • 它允许并发读而无需加锁,在读多写少的场景下拥有极高的性能。
  • 多个 GoRoutine读取、写入和覆盖不相同键的记录时。

在这两种情况下,与使用 MutexRWMutex 的 Map 相比,使用 sync.Map 可以减少锁争用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// https://github.com/golang/go/blob/202a1a57064127c3f19d96df57b9f9586145e21c/src/sync/map_reference_test.go#LL15-L25C2
type mapInterface interface {
	Load(any) (any, bool)                                 // 根据传入的 key 来获取对应 value 值,然后返回 value 值和布尔值(表示是否成功获取)
	Store(key, value any)                                 // 将传入的 key 和 value 值存入 `sync.Map` 中。
	LoadOrStore(key, value any) (actual any, loaded bool) // 如果传入的 key 存在,将会返回已有的 key 对应的 value 值,否则会将指定的 value 存储到 map 中,并返回该 value 和布尔值(表示是否从 map 中拿到)。
	LoadAndDelete(key any) (value any, loaded bool)       // 根据传入的 key 来获取对应 value 值,并从 sync.Map 中移除,最后返回 value 值和布尔值(表示是否成功获取)。
	Delete(any)                                           // 根据传入的 key 来删除 sync.Map 中对应的键值对
	Swap(key, value any) (previous any, loaded bool)      // 输入 key 和 value 值,将 Map 里 key 的 value 值替换为指定的 value 后会返回之前 key 对应的 value 和布尔值(表示是否成功替换)。
	CompareAndSwap(key, old, new any) (swapped bool)      // 将传入的 key 对应的 old value 值替换为新的 value 后会返回布尔值(表示是否替换成功)。
	CompareAndDelete(key, old any) (deleted bool)         // 如果传入的 key 对应的 value 值等于传入的 old value 值,则会从 sync.Map 中删除该 key 及对应 value,并返回布尔值(表示是否成功删除)。
	Range(func(key, value any) (shouldContinue bool))     // 将迭代函数应用于每一个 key-value 对,当函数返回 false 时迭代结束。
}
1
2
3
4
5
6
7
// https://github.com/golang/go/blob/202a1a57064127c3f19d96df57b9f9586145e21c/src/sync/map.go#LL35-L68C2
type Map struct {
	mu sync.Mutex
	read atomic.Pointer[readOnly]
	dirty map[any]*entry
	misses int
}
  • read
    • 读操作可以在不携带mu锁的情况下获取Map中存储的条目,而write操作必须携带mu锁。
    • read 中存储的记录更新可以不用 mu锁,但更新已经被清除的 entry 时,entry 需要被复制到 dirty map 并且在保持Mu时删除。
  • dirty
    • dirty map 必须要用 mu,为了确保 dirty map 可以快速提升为 read map,它还包括 read map 中所有未删除(non-expunged )的条目。
    • 已经被删除的记录将不会被存储在dirty map中。在clean map中的的一个已删除的记录(expunged entry)要想在存储新的值之前,必须先将其解除删除(unexpunged)并加入到dirty map中。
    • 如果 dirty map 为 nil,则下次写入map时将通过创建干净map(clean map)的浅拷贝(a shallow copy)来初始化它,省略过时的条目(stale entries)。
  • misses
    • 计数从上一次更新read map后的读取的次数,这些次数需要锁定mu来确定键是否存在。
    • 一旦发生了足够多的miss而抵消了将dirty map复制的代价,那么就会将dirty map升级为read map(处于未修改状态),并且下一次存储到map里就会制作一个新的dirty map。
  • 总结: Sync.Map 拥有两个内部存储结构(read 和 dirty)。 通过仅使用一个map来读取和第二个用于写入,sync.Map 试图在可能的情况下避免使用mutex。 让我们看看sync.Map操作。
1
2
3
4
5
// https://github.com/golang/go/blob/202a1a57064127c3f19d96df57b9f9586145e21c/src/sync/map.go#LL70-L74C2
type readOnly struct {
	m       map[any]*entry
	amended bool // 如果是true,则 dirty map 中包含一些 read map中没有的数据
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
func (m *Map) Load(key any) (value any, ok bool) {
	read := m.loadReadOnly()  
	e, ok := read.m[key]
	if !ok && read.amended { 
		m.mu.Lock() 
		read = m.loadReadOnly()
		e, ok = read.m[key]
		if !ok && read.amended {
			e, ok = m.dirty[key]
			m.missLocked()
		}
		m.mu.Unlock()
	}
	if !ok {
		return nil, false
	}
	return e.load()
}
  1. sync.Map 的 Load 过程由 Read 和 Dirty 两个阶段组成。
  2. Read 阶段会首先检查在键后是否存在数据。如果数据存在就会被返回。
  3. Dirty 阶段
    1. 开启 mu 互斥锁
    2. 接着 readOnly 结构体(其内容可读)的 amended 字段会被检查,如果其值为 true,则表明 sync.Map 中的 dirty 非空。
    3. missLocked: 对 misses( 对 dirty 的访问计数器)进行自增,而当 misses 的值超过 dirty 的元素数量时, dirty 的元素就会被复制到 read 当中,同时 misses 的值也会重新设置为 0。
  4. e.load 返回数据

为什么要两次调用 m.loadReadOnly()

  • 因为在m.mu上被锁定时,m.dirty有可能被提升为read map,所以要在m.mu上加锁,再次调用m.loadReadOnly()来确保得到正确的结果。

写直接是使用 Swap 方法

1
2
3
func (m *Map) Store(key, value any) {
	_, _ = m.Swap(key, value)
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
func (m *Map) Swap(key, value any) (previous any, loaded bool) {
	read := m.loadReadOnly()    // 调用`m.loadReadOnly()`获取`readOnly的m`
	if e, ok := read.m[key]; ok {  // 检查`m`的`key`是否存在,若存在,尝试使用`trySwap`方法将新值`value`赋给`key`,并返回原值。
		if v, ok := e.trySwap(&value); ok {
			if v == nil {
				return nil, false
			}
			return *v, true
		}
	}
	m.mu.Lock()     // 然后加锁,再获取一次只读版本的`m`并检查其中`key`是否存在,若存在则调用`unexpungeLocked`方法激活对应元素以实现读取或写入
	read = m.loadReadOnly()
	if e, ok := read.m[key]; ok {
		if e.unexpungeLocked() {
			m.dirty[key] = e
		}
		// 之后,再调用`swapLocked`来尝试用新值`value`替换旧值。如果替换成功,则将新值存入变量`previous` 与 `loaded = true`
		if v := e.swapLocked(&value); v != nil {  
			loaded = true
			previous = *v
		}
	// 如果 `dirty[key]`不存在,但`read.m[key]`存在,则将`key`存入`dirty[key]`中,并将`amended`置为`true`
	} else if e, ok := m.dirty[key]; ok {
		if v := e.swapLocked(&value); v != nil {
			loaded = true
			previous = *v
		}
	} else {
	  // 如果`dirty map`为空,则将新的`key`添加到`dirty`中, 并 `read.amended`置为`true`
		if !read.amended {
			m.dirtyLocked()
			m.read.Store(&readOnly{m: read.m, amended: true})
		}
		m.dirty[key] = newEntry(value)
	}
	// 最后解锁,并返回当前`key`的值 和 是否替换成功的标志变量`loaded`
	m.mu.Unlock()
	return previous, loaded
}

删除调用的是 LoadAndDelete 方法

1
2
3
func (m *Map) Delete(key any) {
	m.LoadAndDelete(key)
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
func (m *Map) LoadAndDelete(key any) (value any, loaded bool) {
	read := m.loadReadOnly()  // 获取当前的readOnly
	e, ok := read.m[key]      // 使用`read.m[key]`检测给定键是否存在。
	if !ok && read.amended {
		m.mu.Lock()
		read = m.loadReadOnly()  // 如果给定键不存在且`read.amended`为真,则获取锁并重新获取`read`。
		e, ok = read.m[key]
		if !ok && read.amended {  // 如果给定键不存在且`read.amended`为真,则从`m.dirty`中删除给定键并记录丢失。
			e, ok = m.dirty[key]
			delete(m.dirty, key)
			m.missLocked()
		}
		m.mu.Unlock()
	}
	if ok {
		return e.delete()  // 如果给定键存在,则执行`delete()`函数以删除给定键的值,并返回该值以及'loaded=true'标志位。
	}
	return nil, false
}

相关内容