专注于 JetBrains IDEA 全家桶,永久激活,教程
持续更新 PyCharm,IDEA,WebStorm,PhpStorm,DataGrip,RubyMine,CLion,AppCode 永久激活教程

Golang sync.Map是如何做到尽量避免使用锁?

sync.Map就像Go中的原生map,但是是线程安全的在没有锁或者协调的多goroutines中。加载,存储和删除以摊销的固定时间运行。

Map类型针对两种常见用例进行了优化:(1)给定键的条目仅写入一次但多次读取(如仅在高速缓存中的高速缓存中),或(2)当多个goroutine进行读取,写入和覆盖不相交的键集的条目。在这两种情况下,与单独的Mutex或RWMutex配对的Go map相比,使用sync.Map可以显着减少锁争用。 一个Map在首次使用后,禁止拷贝。


type Map struct { mu Mutex // read contains the portion of the map's contents that are safe for // concurrent access (with or without mu held). // read包含map内容中可以并行访问的部分内容 // The read field itself is always safe to load, but must only be stored with // mu held. // read 字段自身是可以安全加载的,但是在装载的时候必须使用锁 // Entries stored in read may be updated concurrently without mu, but updating // a previously-expunged entry requires that the entry be copied to the dirty // map and unexpunged with mu held. // 可以不使用mu同时更新以read形式存储的条目, // 但是更新以前删除的条目要求将该条目复制到脏映射,并且在保留mu的情况下不删除它。 read atomic.Value // readOnly // dirty包含需要mu保护的map内容,为了确保可以将脏映射快速提升到读取映射,它还包括read // map中的所有未删除条目。 // dirty contains the portion of the map's contents that require mu to be // held. To ensure that the dirty map can be promoted to the read map quickly, // it also includes all of the non-expunged entries in the read map. // 已删除条目不存在dirty map中, // 必须先清除干净映射中的已删除条目并将其添加到脏映射中,然后才能将新值存储到脏映射中。 // Expunged entries are not stored in the dirty map. An expunged entry in the // clean map must be unexpunged and added to the dirty map before a new value // can be stored to it. // 如果脏映射为nil,则对映射的下一次写入将通过创建干净映射的浅表副本来初始化它,并忽略陈旧的条目。 // If the dirty map is nil, the next write to the map will initialize it by // making a shallow copy of the clean map, omitting stale entries. dirty map[interface{}]*entry // misses counts the number of loads since the read map was last updated that // needed to lock mu to determine whether the key was present. // misses计数因为read map上次更新导致需要锁来决定key是否存在 // Once enough misses have occurred to cover the cost of copying the dirty // map, the dirty map will be promoted to the read map (in the unamended // state) and the next store to the map will make a new dirty copy. // 一旦到了足够的次数就触发将dirty map转化为read map,当再有store时,重新生成一个dirty map misses int } // readOnly is an immutable struct stored atomically in the Map.read field. type readOnly struct { m map[interface{}]*entry amended bool // true if the dirty map contains some key not in m. } // expunged is an arbitrary pointer that marks entries which have been deleted // from the dirty map. // 标记元素删除 var expunged = unsafe.Pointer(new(interface{})) // An entry is a slot in the map corresponding to a particular key. type entry struct { // p points to the interface{} value stored for the entry. // // If p == nil, the entry has been deleted and m.dirty == nil. // // If p == expunged, the entry has been deleted, m.dirty != nil, and the entry // is missing from m.dirty. // // Otherwise, the entry is valid and recorded in m.read.m[key] and, if m.dirty // != nil, in m.dirty[key]. // // An entry can be deleted by atomic replacement with nil: when m.dirty is // next created, it will atomically replace nil with expunged and leave // m.dirty[key] unset. // // An entry's associated value can be updated by atomic replacement, provided // p != expunged. If p == expunged, an entry's associated value can be updated // only after first setting m.dirty[key] = e so that lookups using the dirty // map find the entry. p unsafe.Pointer // *interface{} } func newEntry(i interface{}) *entry { return &entry{p: unsafe.Pointer(&i)} }

read这个原生字典可以被看作一个快照,它总会在条件满足时,去重新保存所属的sync.Map值中包含的所有键值对。read字典虽然不会增减其中的键,但却允许变更其中的键所对应的值。所以,它并不是传统意义上的快照,它的只读特性只是对于其中键的集合而言的。

dirty字段代表的原生字典,它存储键值对的方式与read字段中的原生字典一致,它的键类型也是interface{},并且同样是把值先做转换和封装后再进行储存的。我们暂且把它称为脏字典。

注意,脏字典和只读字典如果都存有同一个键值对,那么这里的两个键指的肯定是同一个基本值,对于两个值来说也是如此。

正如前文所述,这两个字典在存储键和值的时候都只会存入它们的某个指针,而不是基本值。

sync.Map在查找指定的键所对应的值的时候,总会先去只读字典中寻找,并不需要锁定互斥锁。只有当确定“只读字典中没有,但脏字典中可能会有这个键”的时候,它才会在锁的保护下去访问脏字典。

// Store sets the value for a key.
func (m *Map) Store(key, value interface{}) {
    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 {
        if e.unexpungeLocked() {
            // The entry was previously expunged, which implies that there is a
            // non-nil dirty map and this entry is not in it.
            m.dirty[key] = e
        }
        e.storeLocked(&value)
    } else if e, ok := m.dirty[key]; ok {
        e.storeLocked(&value)
    } else {
        if !read.amended {
            // We're adding the first new key to the dirty map.
            // Make sure it is allocated and mark the read-only map as incomplete.
            m.dirtyLocked()
            m.read.Store(readOnly{m: read.m, amended: true})
        }
        m.dirty[key] = newEntry(value)
    }
    m.mu.Unlock()
}

相对应的,sync.Map在存储键值对的时候,只要只读字典中已存有这个键,并且该键值对未被标记为“已删除”,就会把新值存到里面并直接返回,这种情况下也不需要用到锁。否则,它才会在锁的保护下把键值对存储到脏字典中。这个时候,该键值对的“已删除”标记会被抹去。

func (e *entry) load() (value interface{}, ok bool) {
    p := atomic.LoadPointer(&e.p)
    if p == nil || p == expunged {
        return nil, false
    }
    return *(*interface{})(p), true
}

顺便说一句,只有当一个键值对应该被删除,但却仍然存在于只读字典中的时候,才会被用标记为“已删除”的方式进行逻辑删除,而不会直接被物理删除。

// Delete deletes the value for a key.
func (m *Map) Delete(key interface{}) {
    read, _ := m.read.Load().(readOnly)
    e, ok := read.m[key]
    if !ok && read.amended {
        m.mu.Lock()
        read, _ = m.read.Load().(readOnly)
        e, ok = read.m[key]
        if !ok && read.amended {
            delete(m.dirty, key)
        }
        m.mu.Unlock()
    }
    if ok {
        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会先去检查只读字典中是否有对应的键。如果没有,脏字典中可能有,那么它就会在锁的保护下,试图从脏字典中删掉该键值对。

最后,sync.Map会把该键值对中指向值的那个指针置为nil,这是另一种逻辑删除的方式。

除此之外,还有一个细节需要注意,只读字典和脏字典之间是会互相转换的。

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
}

在脏字典中查找键值对次数足够多的时候,sync.Map会把脏字典直接作为只读字典,保存在它的read字段中,然后把代表脏字典的dirty字段的值置为nil。

在这之后,一旦再有新的键值对存入,它就会依据只读字典去重建脏字典。这个时候,它会把只读字典中已被逻辑删除的键值对过滤掉。理所当然,这些转换操作肯定都需要在锁的保护下进行。

综上所述,sync.Map的只读字典和脏字典中的键值对集合,并不是实时同步的,它们在某些时间段内可能会有不同。

由于只读字典中键的集合不能被改变,所以其中的键值对有时候可能是不全的。

相反,脏字典中的键值对集合总是完全的,并且其中不会包含已被逻辑删除的键值对。

因此,可以看出,在读操作有很多但写操作却很少的情况下,并发安全字典的性能往往会更好。

在几个写操作当中,新增键值对的操作对并发安全字典的性能影响是最大的,

其次是删除操作,

最后才是修改操作。

如果被操作的键值对已经存在于sync.Map的只读字典中,并且没有被逻辑删除,那么修改它并不会使用到锁,对其性能的影响就会很小。

内容来源:

  • 部分内容摘自郝林老师的极客时间

文章永久链接:https://tech.souyunku.com/41002

未经允许不得转载:搜云库技术团队 » Golang sync.Map是如何做到尽量避免使用锁?

JetBrains 全家桶,激活、破解、教程

提供 JetBrains 全家桶激活码、注册码、破解补丁下载及详细激活教程,支持 IntelliJ IDEA、PyCharm、WebStorm 等工具的永久激活。无论是破解教程,还是最新激活码,均可免费获得,帮助开发者解决常见激活问题,确保轻松破解并快速使用 JetBrains 软件。获取免费的破解补丁和激活码,快速解决激活难题,全面覆盖 2024/2025 版本!

联系我们联系我们