sync.Map源码详解
sync.Map的设计非常精简,只使用了三个结构体和一个全局变量,可读性非常高,配合源码中的注释基本可以清楚其实现逻辑。
文章中涉及到的源码均来自sdk 1.19.4版本。
帮助理解
为了帮助大家快速理解,先用大白话简单描述下sync.Map的实现原理。
sync.Map中有两个map容器,一个是只读的read map,另一个是可读可写的dirty map。
当一个查询请求到来时,会先到read map中查询,如果找不到则会穿透到dirty map中查询,并且穿透计数器加一,表示这是一次穿透查询。
每次查询的时候都会走一遍这个逻辑,直到穿透查询的次数超过了此时dirty map的长度,则会执行“dirty map提升”操作,将dirty map赋值给read map,并且将dirty map置为nil。
当一个新增或者更新的请求到来时,会先到read map中查询,如果查到了就直接更新;如果不存在就到dirty map中查询,查到了就直接更新;如果不存在说明这个key在read map和dirty map中都不存在,则走新增逻辑。新增的时候是直接写到dirty map中,并且告诉read map,我(dirty map)这里有你没有的key。
核心数据结构Map
type Map struct {
// 锁结构
mu Mutex
// read存的是一个叫做readOnly的不可导出结构体,只读,并且由于保存atomic.Value中,所以读readOnly是并发安全的。
read atomic.Value
// 通过Store()方法存储的数据都会保存在这里,也就是所有新增的数据都保存在这里
dirty map[any]*entry
// 在read中找不到key的次数,如果misses>len(dirty),dirty中的数据会被保存到read中,然后将dirty置为nil
misses int
}
readOnly结构体
readOnly是实现read map的结构体
type readOnly struct {
m map[interface{}]*entry // readOnly中的只读map
amended bool // 为true时表示dirty中包含了m中没有的key。为false时表示dirty为nil。
}
entry结构体
entry是保存value的结构体
type entry struct {
// p是一个指针类型,这说明map中保存的是value值的地址:
// 如果value的值类型为结构体指针,保存的是指针的地址,如果值类型为结构体,保存的是结构体的地址
p unsafe.Pointer // *interface{}
}
全局变量
// 一个指针变量,标记某一个key是否已经删除。被删除key的entry.p会被赋值为expunged
var expunged = unsafe.Pointer(new(any))
增删改查源码导读
查询:Load
// 查询方法
func (m *Map) Load(key any) (value any, ok bool) {
read, _ := m.read.Load().(readOnly) // 从read中获取只读map
e, ok := read.m[key]
if !ok && read.amended { // 如果不存在并且dirty中存在新数据
m.mu.Lock() // 加锁
read, _ = m.read.Load().(readOnly) // 二次确认,防止加锁前到加锁成功的窗口期,read中的数据发生变化
e, ok = read.m[key]
if !ok && read.amended { // read中不存在并且dirty中存在新数据,则到dirty中查询该key
e, ok = m.dirty[key]
m.missLocked() // 记录一次miss,当miss的次数超过dirty的长度,会将dirty提升给read map
}
m.mu.Unlock()
}
if !ok { // 如果dirty中也没有该key,返回nil和false
return nil, false
}
return e.load()
}
当read中查询不到key而穿透到dirty中时,会调用missLocked()方法将Map.misses变量加1,表示查询穿透了一次,如果穿透的次数大于dirty的长度,会触发“dirty map提升”操作,也就是将dirty存储的map赋值给read,并且将dirty置为nil。
func (m *Map) missLocked() {
m.misses++ // 穿透查询次数+1
if m.misses < len(m.dirty) { // 如果穿透查询的次数小于dirty的长度,直接返回
return
}
// dirty map提升操作
m.read.Store(readOnly{m: m.dirty})
m.dirty = nil
m.misses = 0
}
增改:Store
// Store sets the value for a key.
func (m *Map) Store(key, value any) {
// 如果read中存在key,则直接更新
read, _ := m.read.Load().(readOnly)
if e, ok := read.m[key]; ok && e.tryStore(&value) {
return
}
m.mu.Lock()
read, _ = m.read.Load().(readOnly)
if e, ok := read.m[key]; ok { // 如果read中存在key并且未标记删除,则直接更新,同时双写到dirty map
if e.unexpungeLocked() {
m.dirty[key] = e
}
e.storeLocked(&value)
} else if e, ok := m.dirty[key]; ok { // 如果read中不存在,dirty中存在,则直接更新dirty
e.storeLocked(&value)
} else { // read和dirty中都不存在key,走新增逻辑
if !read.amended { // 如果dirty为nil,则初始化dirty,并将read中的数据拷贝到dirty中
m.dirtyLocked()
m.read.Store(readOnly{m: read.m, amended: true}) // 将amended置为true,表示dirty map中有新数据
}
m.dirty[key] = newEntry(value)
}
m.mu.Unlock()
}
tryStore:原子CAS操作更新value值
func (e *entry) tryStore(i *any) bool {
for {
p := atomic.LoadPointer(&e.p)
if p == expunged { // 如果p已经被标记删除,返回保存失败
return false
}
if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) { // 原子CAS操作
return true
}
}
}
unexpungeLocked:原子CAS操作判断value值是否被标记为已删除
func (e *entry) unexpungeLocked() (wasExpunged bool) {
return atomic.CompareAndSwapPointer(&e.p, expunged, nil)
}
dirtyLocked:初始化dirty map,将read map中未标记删除的数据拷贝到dirty map中
func (m *Map) dirtyLocked() {
if m.dirty != nil { // 如果dirty已经存在了,说明已经初始化过了,直接返回
return
}
read, _ := m.read.Load().(readOnly)
m.dirty = make(map[any]*entry, len(read.m))
for k, e := range read.m {
if !e.tryExpungeLocked() {
m.dirty[k] = e
}
}
}
tryExpungeLocked:判断read map中的value是否已经被标记删除了
func (e *entry) tryExpungeLocked() (isExpunged bool) {
p := atomic.LoadPointer(&e.p)
// 如果value不存在,则无限重试直到将p赋值为expunged,并返回true
for p == nil {
if atomic.CompareAndSwapPointer(&e.p, nil, expunged) {
return true
}
p = atomic.LoadPointer(&e.p)
}
return p == expunged
}
删除:Delete
// 删除对应的key
func (m *Map) Delete(key any) {
m.LoadAndDelete(key)
}
// 查询并且删除对应的key
func (m *Map) LoadAndDelete(key any) (value any, loaded bool) {
read, _ := m.read.Load().(readOnly) // 先从read中读取
e, ok := read.m[key]
if !ok && read.amended { // 如果read中不存在并且dirty中有新数据
m.mu.Lock()
read, _ = m.read.Load().(readOnly) // 二次确认,防止加锁期间数据更新
e, ok = read.m[key]
if !ok && read.amended { // 如果read中不存在并且dirty中有新数据
e, ok = m.dirty[key]
delete(m.dirty, key) // dirty中存在是直接删除
m.missLocked() // 无论dirty中是否存在key,misses都会计数
}
m.mu.Unlock()
}
if ok {
return e.delete() // read中存在是标记删除
}
return nil, false
}
func (e *entry) delete() (value any, ok bool) {
for {
p := atomic.LoadPointer(&e.p) // 原子操作加载value值
if p == nil || p == expunged { // 如果value值不存在或者已经被标记删除,返回nil和删除失败
return nil, false
}
if atomic.CompareAndSwapPointer(&e.p, p, nil) { // 原子CAS操作将value值置为nil
return *(*any)(p), true
}
}
}
思考题:
1. 为什么misses达到dirty的长度后才将dirty的map赋值给read?
因为dirty的元素一般包含read中的元素,如果misses大于dirty的长度,说明此时read的穿透率已经非常高了,理想情况下穿透率为100%(dirty中的每个元素都被穿透访问了一次)。
2. 为什么在写多读少的高并发场景中,sync.Map的性能比较差?
sync.Map的写操作(Store方法)逻辑比较重,涉及到加锁,多个逻辑判断,以及map拷贝等操作,效率远低于直接对map加锁。
3. read map和dirty map之间的数据集是什么关系?
dirty map中的数据是read map的超集。
以上就是本文的全部内容,如果有不对的地方欢迎指正,如果有疑问也欢迎评论或者私信留言。