警告
本文最后更新于 2023-02-16,文中内容可能已过时。
sync.Map 是Go语言中提供的一种大幅度改进版本的share.Map,它实现了在多个协程中安全读写map的功能。它在内部使用了 Mutex (互斥锁) 来实现数据的安全性,提供的功能如下:
- 快速:它使用了诸如读写加锁权衡,把锁分散,采用读更新一小片的方式,使得整体性能比其他实现更好。
- 安全:它的读写都是安全的,因此它可以作为在多个协程中安全进行读写的工具。
sync.Map 类型主要针对以下场景进行了性能优化:
- 它允许并发读而无需加锁,在读多写少的场景下拥有极高的性能。
- 多个 GoRoutine读取、写入和覆盖不相同键的记录时。
在这两种情况下,与使用 Mutex
或 RWMutex
的 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()
}
|
- sync.Map 的 Load 过程由 Read 和 Dirty 两个阶段组成。
Read
阶段会首先检查在键后是否存在数据。如果数据存在就会被返回。
Dirty
阶段
- 开启
mu
互斥锁
- 接着
readOnly
结构体(其内容可读)的 amended
字段会被检查,如果其值为 true
,则表明 sync.Map 中的 dirty
非空。
missLocked
: 对 misses
( 对 dirty
的访问计数器)进行自增,而当 misses
的值超过 dirty
的元素数量时, dirty
的元素就会被复制到 read
当中,同时 misses
的值也会重新设置为 0。
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
}
|