Go中的sync.Map源码详解

161 阅读6分钟

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的值类型为结构体指针,保存的是指针的地址,如果值类型为结构体,保存的是结构体的地址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的超集。

以上就是本文的全部内容,如果有不对的地方欢迎指正,如果有疑问也欢迎评论或者私信留言。