开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 22 天,点击查看活动详情
sync.map
sync.Map 是 Go 语言中的一个并发安全的 map,它可以用于多个 goroutine 之间的数据共享和访问。相对于标准库中的 map 类型,sync.Map 的一个显著特点是它是并发安全的,多个 goroutine 可以同时读取和修改其中的数据,而不需要额外的锁机制。此外,与标准库的 map 不同,sync.Map 中的键和值都是接口类型,这使得它更加灵活,可以存储任何类型的数据。同时,由于 sync.Map 内部使用了类似于分片锁的技术,因此在并发访问时也能保持较高的性能。
在使用 sync.Map 时,可以使用其内置的 Load、Store、Delete 和 Range 方法来读取、写入、删除和遍历其中的数据。与标准库中的 map 类型不同,这些方法的参数和返回值都是接口类型,因此需要进行类型转换才能访问具体的数据。此外,sync.Map 中的数据不支持使用 len 方法来获取长度,因此如果需要统计其中元素的个数,需要使用其他方式来实现。
基本使用
package main
import (
"fmt"
"sync"
)
func main() {
var m sync.Map
m.Store("name", "Alice")
m.Store("age", 30)
name, ok := m.Load("name")
if ok {
fmt.Println(name)
}
age, ok := m.Load("age")
if ok {
fmt.Println(age)
}
m.Range(func(key, value interface{}) bool {
fmt.Printf("%s: %v\n", key, value)
return true
})
}
源码阅读
type Map struct {
mu Mutex
// read 包含了 map 内容的部分,该部分可被并发访问,无论是否持有 mu。
// read 字段本身是可以安全加载的,但只有持有 mu 时才能进行存储。
// 存储在 read 中的条目可以在没有 mu 的情况下同时进行更新,但更新先前被删除的条目需要将其复制到 dirty map 中,并在持有 mu 时将其恢复。
read atomic.Pointer[readOnly]
// dirty 包含需要持有 mu 才能访问的 map 内容的部分。为了确保 dirty map 可以快速地提升为 read map,它还包括 read map 中的所有未被删除的条目。
// 被删除的条目不会存储在 dirty map 中。在 clean map 中删除的条目必须在添加新值之前被恢复并添加到 dirty map 中。
// 如果 dirty map 为 nil,则下一次对 map 的写操作将通过对 clean map 进行浅拷贝(省略陈旧的条目)来初始化它。
dirty map[any]*entry
// misses 记录了自从上次更新 read map 以来需要锁定 mu 才能确定键是否存在的加载次数。
// 一旦出现足够多的 misses 来覆盖复制 dirty map 的成本,dirty map 将被提升为 read map(处于未修改状态),
// 并且下一次对 map 的 store 将会生成一个新的 dirty map 副本。
misses int
}
// readOnly 是一个不可变的结构体,原子性地存储在 Map.read 字段中。
type readOnly struct {
m map[any]*entry
amended bool // 如果dirty map包含m中没有的键,则为true。
}
// expunged 是一个任意指针,用于标记已从 dirty map 中删除的条目。
var expunged = new(any)
// 一个 entry 表示 map 中的一个特定键对应的槽位。
type entry struct {
// p 指向该条目存储的 interface{} 值。
// 如果 p == nil,则该条目已被删除,且 m.dirty == nil 或 m.dirty[key] == e。
// 如果 p == expunged,则该条目已被删除,m.dirty != nil,且该条目已从 m.dirty 中删除。
// 否则,该条目有效,并记录在 m.read.m[key] 中,在 m.dirty != nil 时也会记录在 m.dirty[key] 中。
// 条目可以通过原子替换为 nil 来删除:当下次创建 m.dirty 时,它将原子替换 nil 为 expunged,并将 m.dirty[key] 保持未设置状态。
// 如果 p != expunged,则可以通过原子替换来更新条目的关联值。如果 p == expunged,则只能在首先设置 m.dirty[key] = e 以便使用 dirty map 查找条目后才能更新条目的关联值。
p atomic.Pointer[any]
}
mu: Mutex用于保护map的读写操作,确保线程安全。read: atomic.Value用于存储只读状态下的map。在一个线程需要读取map的时候,会先检查这个值,如果不为空则表示这个线程可以直接读取这个值,不需要获取锁。如果为空,则需要获取锁,然后从dirty中获取值。dirty: 存储脏数据的map,即在写操作中被修改但是还没有被写回read的数据。这个map并没有被保护,因为在写操作中,会先获取锁,确保写操作的线程安全。misses: 记录在dirty中未找到的键值对的数量,即缺失的次数,用于调整dirty的大小。
Load
func (m *Map) loadReadOnly() readOnly {
if p := m.read.Load(); p != nil {
return *p
}
return readOnly{}
}
// Load 方法返回给定 key 在 Map 中对应的 value,如果不存在则返回 nil。
// ok 参数表示是否在 Map 中找到了对应的 value。
func (m *Map) Load(key any) (value any, ok bool) {
// 原子读取
read := m.loadReadOnly()
e, ok := read.m[key]
// read不存在,dirty可能存在,去dirty看看
if !ok && read.amended {
m.mu.Lock()
// 如果我们在 m.mu 上被阻塞时 m.dirty 得到了晋升,避免报告虚假的缺失。
// (如果进一步的对同一个键的加载不会丢失,那么为这个键复制脏映射就不值得。)
read = m.loadReadOnly()
e, ok = read.m[key]
// 二次确认
if !ok && read.amended {
e, ok = m.dirty[key]
// 无论条目是否存在,都记录一次未命中:直到将 dirty map 提升为 read map,这个 key 都将采用慢路径。
m.missLocked()
}
m.mu.Unlock()
}
// dirty也没有
if !ok {
return nil, false
}
// read或dirty有,返回结果
return e.load()
}
// miss+1,并查看是否需要刷新read
func (m *Map) missLocked() {
m.misses++
// miss 次数小于dirty长度先不刷新
if m.misses < len(m.dirty) {
return
}
// 将dirty刷新到read
m.read.Store(&readOnly{m: m.dirty})
m.dirty = nil
m.misses = 0
}
// 返回结果
func (e *entry) load() (value any, ok bool) {
p := e.p.Load()
if p == nil || p == expunged {
return nil, false
}
return *p, true
}
Load:
- 先看看read中是否有
- 如果没有加锁去dirty查看,并且double check
- 无论有没有,miss+1,当miss>=dirty.len时将dirty刷新到read
- 返回结果
Store
// Store 方法用于为一个键设置值。
func (m *Map) Store(key, value any) {
_, _ = m.Swap(key, value)
}
// Swap 交换一个键的值并返回原来的值(如果有的话)。
// loaded 的结果报告键是否存在。
func (m *Map) Swap(key, value any) (previous any, loaded bool) {
read := m.loadReadOnly()
// read中有
if e, ok := read.m[key]; ok {
if v, ok := e.trySwap(&value); ok {
if v == nil {
return nil, false
}
return *v, true
}
}
// read中没有或操作失败
m.mu.Lock()
read = m.loadReadOnly()
// read中有
if e, ok := read.m[key]; ok {
// 之前被删除过
if e.unexpungeLocked() {
// 该条目先前被删除,这意味着有一个非空的 dirty map,而这个条目不在其中。
m.dirty[key] = e
}
if v := e.swapLocked(&value); v != nil {
loaded = true
previous = *v
}
// dirty中有
} else if e, ok := m.dirty[key]; ok {
if v := e.swapLocked(&value); v != nil {
loaded = true
previous = *v
}
} else {
if !read.amended {
// 我们将第一个新的键添加到了 dirty map 中。
// 确保它已被分配,并将只读 map 标记为不完整。
m.dirtyLocked()
m.read.Store(&readOnly{m: read.m, amended: true})
}
m.dirty[key] = newEntry(value)
}
m.mu.Unlock()
return previous, loaded
}
Delete
// LoadAndDelete 方法会删除指定 key 对应的 value 并返回其值(如果存在)。
// loaded 参数会返回 key 是否存在于 Map 中。
func (m *Map) LoadAndDelete(key any) (value any, loaded bool) {
read := m.loadReadOnly()
e, ok := read.m[key]
if !ok && read.amended {
m.mu.Lock()
// double check
read = m.loadReadOnly()
e, ok = read.m[key]
if !ok && read.amended {
e, ok = m.dirty[key]
// 在dirty中删除
delete(m.dirty, key)
// 无论entry是否存在,这里都会记录一次miss,因为这个key在dirty map中被删除了,需要在读写锁被持有时,
// 将其从dirty map复制到read map中。因此,下一次访问这个key时,需要经过一次较慢的查找操作。
m.missLocked()
}
m.mu.Unlock()
}
// 在read中删除
if ok {
return e.delete()
}
return nil, false
}
// Delete 删除指定 key 的值。
func (m *Map) Delete(key any) {
m.LoadAndDelete(key)
}
总结
空间换时间,引入额外的dirty,减少了read的压力,read不需要加锁。 以下两个场景中使用 sync.Map,会比使用 map+RWMutex 的方式,性能要好得多:
- 只会增长的缓存系统中,一个 key 只写入一次而被读很多次;
- 多个 goroutine 为不相交的键集读、写和重写键值对。