Sync.Map
Golang 的 sync.Map 是一个并发安全的 map 实现,在 Go 1.9 版本中被引入。它的设计思想和底层实现如下:
设计思想
- 读写分离 sync.Map 将读和写操作分离到不同的数据结构中,避免读写冲突。读操作主要在 read 视图中进行,写操作则在 dirty 视图中进行。
- 空间换时间 sync.Map 通过冗余的数据结构(read 和 dirty),以空间换取更好的读写性能。
- 双重检查 在读取数据时,sync.Map 先检查 read 视图,如果没有则检查 dirty 视图,避免读遗漏。
- 延迟删除 删除操作只是将数据从 read 视图中标记为已删除,而不是立即从内存中删除,减少删除操作的开销。
底层实现
- 数据结构
type Map struct {
// 该锁⽤来保护dirty
mu Mutex
// 存读的数据,因为是atomic.value类型,只读类型,所以它的读是并发安全的
read atomic.Value // readOnly
//包含最新的写⼊的数据,并且在写的时候,会把read 中未被删除的数据拷⻉到该dirty中,因为是普通的map
存在并发安全问题,需要⽤到上⾯的mu字段。
dirty map[interface{}]*entry
// 一个原子操作的计数器,用于统计 read 视图读取 miss 的次数。当 misses 达到一定阈值时,sync.Map 会将 dirty 视图提升为新的 read 视图。
// 从read读数据的时候,会将该字段+1,当等于len(dirty)的时候,会将dirty拷⻉到read中(从⽽提升
读的性能)。
misses int
}
read 数据结构
type readOnly struct {
m map[interface{}]*entry
// 如果Map.dirty的数据和m 中的数据不⼀样是为true
amended bool
}
entry 数据结构
type entry struct {
//可⻅value是个指针类型,虽然read和dirty存在冗余情况(amended=false),但是由于是指针类型,存
储的空间应该不是问题
p unsafe.Pointer // *interface{}
}
sync.Map 使用 read 和 dirty 两个视图存储数据,前者用于读操作,后者用于写操作。 2. 读取流程 先在 read 视图中查找键值对,如果没有则在 dirty 视图中查找。如果 dirty 视图有新数据,则需要先将其提升(promote)到 read 视图。
func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
//因read只读,线程安全,先查看是否满⾜条件
read, _ := m.read.Load().(readOnly)
e, ok := read.m[key]
//如果read没有,并且dirty有新数据,那从dirty中查找,由于dirty是普通map,线程不安全,这个时候⽤
到互斥锁了
if !ok && read.amended {
m.mu.Lock()
// 双重检查
read, _ = m.read.Load().(readOnly)
e, ok = read.m[key]
// 如果read中还是不存在,并且dirty中有新数据
if !ok && read.amended {
e, ok = m.dirty[key]
// mssLocked()函数是性能是sync.Map 性能得以保证的重要函数,⽬的讲有锁的dirty数据,
替换到只读线程安全的read⾥
m.missLocked()
}
m.mu.Unlock()
}
if !ok {
return nil, false
}
return e.load()
}
//dirty 提升⾄read 关键函数,当misses 经过多次因为load之后,⼤⼩等于len(dirty)时候,讲dirty替换到read⾥,以此达到性能提升。
func (m *Map) missLocked() {
m.misses++
if m.misses < len(m.dirty) {
return
}
//原⼦操作,耗时很⼩
m.read.Store(readOnly{m: m.dirty})
m.dirty = nil
m.misses = 0
}
- 写入流程 新的键值对直接写入 dirty 视图。当 read 视图访问次数达到阈值时,sync.Map 会将 dirty 视图中的新数据提升到 read 视图。
func (m *Map) Store(key, value interface{}) {
// 如果m.read存在这个key,并且没有被标记删除,则尝试更新。
read, _ := m.read.Load().(readOnly)
if e, ok := read.m[key]; ok && e.tryStore(&value) {
return
}
// 如果read不存在或者已经被标记删除
m.mu.Lock()
read, _ = m.read.Load().(readOnly)
if e, ok := read.m[key]; ok {
//如果entry被标记expunge,则表明dirty没有key,可添加⼊dirty,并更新entry
if e.unexpungeLocked() {
//加⼊dirty中
m.dirty[key] = e
}
//更新value值
e.storeLocked(&value)
//dirty 存在该key,更新
} else if e, ok := m.dirty[key]; ok {
e.storeLocked(&value)
//read 和dirty都没有,新添加⼀条
} else {
//dirty中没有新的数据,往dirty中增加第⼀个新键
if !read.amended {
//将read中未删除的数据加⼊到dirty中
m.dirtyLocked()
m.read.Store(readOnly{m: read.m, amended: true})
}
m.dirty[key] = newEntry(value)
}
m.mu.Unlock()
}
//将read中未删除的数据加⼊到dirty中
func (m *Map) dirtyLocked() {
if m.dirty != nil {
return
}
read, _ := m.read.Load().(readOnly)
m.dirty = make(map[interface{}]*entry, len(read.m))
//read如果较⼤的话,可能影响性能
for k, e := range read.m {
//通过此次操作,dirty中的元素都是未被删除的,可⻅expunge的元素不在dirty中
if !e.tryExpungeLocked() {
m.dirty[k] = e
}
}
}
//判断entry是否被标记删除,并且将标记为nil的entry更新标记为expunge
func (e *entry) tryExpungeLocked() (isExpunged bool) {
p := atomic.LoadPointer(&e.p)
for p == nil {
// 将已经删除标记为nil的数据标记为expunged
if atomic.CompareAndSwapPointer(&e.p, nil, expunged) {
return true
}
p = atomic.LoadPointer(&e.p)
}
return p == expunged
}
//对entry 尝试更新
func (e *entry) tryStore(i *interface{}) bool {
p := atomic.LoadPointer(&e.p)
if p == expunged {
return false
}
for {
if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
return true
}
p = atomic.LoadPointer(&e.p)
if p == expunged {
return false
}
}
}
//read⾥ 将标记为expunge的更新为nil
func (e *entry) unexpungeLocked() (wasExpunged bool) {
return atomic.CompareAndSwapPointer(&e.p, expunged, nil)
}
//更新entry
func (e *entry) storeLocked(i *interface{}) {
atomic.StorePointer(&e.p, unsafe.Pointer(i))
}
- 删除流程 删除操作只是将数据从 read 视图中标记为已删除,而不会立即从内存中删除,减少删除操作的开销。
func (m *Map) Delete(key interface{}) {
read, _ := m.read.Load().(readOnly)
e, ok := read.m[key]
//如果read中没有,并且dirty中有新元素,那么就去dirty中去找
if !ok && read.amended {
m.mu.Lock()
//这是双检查(上⾯的if判断和锁不是⼀个原⼦性操作)
read, _ = m.read.Load().(readOnly)
e, ok = read.m[key]
if !ok && read.amended {
//直接删除
delete(m.dirty, key)
}
m.mu.Unlock()
}
if ok {
//如果read中存在该key,则将该value 赋值nil(采⽤标记的⽅式删除!)
e.delete()
}
}
func (e *entry) delete() (hadValue bool) {
for {
p := atomic.LoadPointer(&e.p)
if p == nil || p == expunged {
return false
}
if atomic.CompareAndSwapPointer(&e.p, p, nil) {
return true
}
}
}
综上所述,sync.Map 通过读写分离、空间换时间、双重检查和延迟删除等优化策略,实现了高性能的并发安全 map。但对于频繁增删的场景,其性能可能不如使用 sync.RWMutex 加锁的方式
性能对比
-
sync.map
- 读多写少
- sync.Map 的结构优化了读取操作,适合在数据被频繁读取但不常更新的场景中使用。例如,缓存数据或配置值的存储。
- 数据只增不减的情况
- 在某些情况下,数据只会增加而不会减少,sync.Map 的性能表现会更优,因为写入操作不会频繁导致锁竞争。
- 高并发访问
- 对于多个 goroutine 同时访问的情况,sync.Map 能够有效减少锁的争用,提供更好的性能。
- 读多写少
-
map 加锁
- 写操作频繁
- 当需要频繁写入数据时,使用 sync.Mutex 或 sync.RWMutex 可以更好地控制并发,尽管会引入锁的开销,但在写操作较多的情况下,性能会更稳定。
- 复杂的读写逻辑
- 如果需要在读写操作中进行复杂的逻辑处理,使用加锁的 map 可以更灵活地控制并发行为。
- 数据一致性要求高
- 在需要严格保证数据一致性的场景中,使用加锁的方式可以确保在读写操作中不会出现数据不一致的问题
- 写操作频繁
-
分段锁(concurrent-map)
-
性能对比
- 读性能 sync.Map 在读操作上表现优越,特别是在高并发的读场景中,能够通过原子操作和读缓存机制提供快速访问。
- 写性能 sync.Map 的写性能相对较差,尤其是在频繁写入的情况下,可能导致性能下降,因为写操作需要加锁,且会影响到读操作的效率。
- 锁竞争 使用 sync.Mutex 或 sync.RWMutex 的 map 在写操作时会引入锁竞争,可能导致性能瓶颈,尤其在高并发环境下。
- 适用性 sync.Map 更适合于读多写少的场景,而加锁的 map 则在写操作频繁的情况下表现更好.