通过read map和dirty map 将读写分离,实现高效读写
如果read map读取不到并且amended为true(false表示read map和dirty map一致,就没必要再读dirty map了),则给map加锁并从dirty map读取,将misses+1。如果map中一共有n个元素,但是读了n次都没有在read map中找到(就是misses的值大于等于map的长度),则会将dirty map升级为 read map ,dirty map 重置为nil,misses重置为0;
对于value的更新,优先使用read map 原子模式(因为value指向的是entry指针,属于原子类型)实现高效更新。如果read map中没有找到,并且amended是false,代表这个元素真的不存在,怎么办?map上锁并写入dirty map,并将amended设置为true ,代表read map和dirty map 不一致。这里还有一个问题,在读的时候我们说过,在一定情况下,dirty map被置为nil了,你怎么往nil里面写呢?诶,在写之前,会通过dirtyLocked
方法查一下dirty map是不是nil,是nil的话就把read map里面的值拷贝给dirty map 。
删除的时候优先查找read map并删除。如果read map中没有,并且 amended为true,则取删除dirty map的元素,将misses+1。如果条件满足则升级dirty map。
关于expunged和nil 很多网上的文章将expunged和nil 讲的稀碎,估计是自己也没搞懂。我们要先有一个共识,就是expunged和nil都是属于被删除的状态。然后我再说一个问题,我们知道,在一定条件下,dirty map升级为read map ,然后dirty map被置为nil,这个时候如果有delete操作,在read map中就会被标记为nil,那么这个时候,来了一个store操作,read map中的元素会被循环写入到新的dirty map中,read map中的nil 肯定不会被拷贝,那么我们怎么标记这个元素呢?继续用nil肯定不行,因为它不属于dirty map中的删除操作啊。怎么办?那么就在read map向dirty map拷贝的时候,将nil 转换为expunged就行了。
有人问,expunged什么时候会被真正的删除呢?如果你有这样的问题,就再读一遍本文!
如果要详细聊这个问题,那就要说一说golang中map的问题以及为什么会出现sync.map。
在go里面,map 是不支持并发写操作的,我们看下面一个例子
/usr/local/Cellar/go/1.17.5/libexec/src/runtime/map.go
中我们看到map的数据结构,我们看到几个重要的属性
type bucket struct {
//next 是一个指向下一个 bucket 的指针
next *bucket
//allnext 是一个指向所有 bucket 的指针,用于遍历所有的 bucket。
allnext *bucket
//typ 是一个枚举类型,表示 bucket 的类型,有 memBucket 或 blockBucket 两种
typ bucketType // memBucket or blockBucket (includes mutexProfile)
//hash 是一个无符号整数,表示 bucket 的哈希值,用于在哈希表中定位。
hash uintptr
//size 是一个无符号整数,表示 bucket 的大小,即记录数据的大小。
size uintptr
//nstk 是一个无符号整数,表示 bucket 的栈深度,即栈字的个数。
nstk uintptr
}
type MapNew struct {
sync.RWMutex
M map[int]int
}
sync.Map 是 Go 语言标准库中提供的一个并发安全的 Map 实现。它的设计目标是在多个 goroutine 之间共享数据时,提供高效的读写操作。
type Map struct {
//更新dirty map和miss计算器的时候加锁
mu Mutex
// 读map 读这个map里面的任何元素都是安全的,和互斥锁没有关系,但是如果要写这个map,必须要上互斥锁。
read atomic.Value // readOnly
//// 最新写入的数据
dirty map[interface{}]*entry
//// 计数器,每次需要读 dirty 则 +1,到达一定次数则会将写map升级为读map
misses int
}
type readOnly struct {
// 内建 map
m map[interface{}]*entry
// 如果写map中存在读map中不存在的key,标记为true
amended bool
}
type entry struct {
//该指针指向真正的值的地址
//指针有三种形态,一个是p==nil,表示这个entry已经被删除了,并且m.dirty==nil
//p==expunged entry已经被清除了,但是m.dirty!=nil
p unsafe.Pointer // 等同于 *interface{}
}
m.misses >= len(m.dirty)
,这个时候,写map的m就会复制一份给读map,为了避免这种情况,我们先拿到锁,确定写map不在升级过程中,然后再次读取一次读map,真的没有了,才去写map查找。func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
read, _ := m.read.Load().(readOnly)
e, ok := read.m[key]
if !ok && read.amended {
m.mu.Lock()
// Avoid reporting a spurious miss if m.dirty got promoted while we were
// blocked on m.mu. (If further loads of the same key will not miss, it's
// not worth copying the dirty map for this key.)
read, _ = m.read.Load().(readOnly)
e, ok = read.m[key]
if !ok && read.amended {
e, ok = m.dirty[key]
// Regardless of whether the entry was present, record a miss: this key
// will take the slow path until the dirty map is promoted to the read
// map.
m.missLocked()
}
m.mu.Unlock()
}
if !ok {
return nil, false
}
return e.load()
}
// 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()
}
// Delete deletes the value for a key.
func (m *Map) Delete(key interface{}) {
m.LoadAndDelete(key)
}
func (m *Map) Range(f func(key, value interface{}) bool) {
// We need to be able to iterate over all of the keys that were already
// present at the start of the call to Range.
// If read.amended is false, then read.m satisfies that property without
// requiring us to hold m.mu for a long time.
read, _ := m.read.Load().(readOnly)
if read.amended {
// m.dirty contains keys not in read.m. Fortunately, Range is already O(N)
// (assuming the caller does not break out early), so a call to Range
// amortizes an entire copy of the map: we can promote the dirty copy
// immediately!
m.mu.Lock()
read, _ = m.read.Load().(readOnly)
if read.amended {
read = readOnly{m: m.dirty}
m.read.Store(read)
m.dirty = nil
m.misses = 0
}
m.mu.Unlock()
}
for k, e := range read.m {
v, ok := e.load()
if !ok {
continue
}
if !f(k, v) {
break
}
}
}